diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml new file mode 100644 index 0000000..3f4ce5f --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,24 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" + - name: build + run: go build -v ./... + - name: e2e test + run: go test -v ./e2e/... -count=1 diff --git a/README.md b/README.md index e743575..b6c1e9c 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,28 @@ ## About this project -Testing framework for openMCP +OpenMCP-testing helps to set up e2e test suites for openmcp applications. Like [xp-testing](https://github.com/crossplane-contrib/xp-testing) but for [openmcp](https://github.com/openmcp-project). + +* [`pkg/clusterutils`](./pkg/clusterutils/) provides functionality to interact with the different clusters of an openMCP installation +* [`pkg/conditions`](./pkg/conditions/) provides common pre/post condition checks +* [`pkg/providers`](./pkg/providers/) provides functionality to test cluster-providers, platform-services and service-providers +* [`pkg/resources`](./pkg/resources/) provides functionality to (batch) import and delete resources +* [`pkg/setup`](./pkg/setup/) provides functionality to bootstrap an openmcp environment ## Requirements and Setup -*Insert a short description what is required to get your project running...* +You need [go](https://go.dev/) and [docker](https://www.docker.com/) to execute the sample test suite. + +```shell + go test -v ./e2e/... +``` ## Support, Feedback, Contributing This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/openmcp-project/openmcp-testing/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). ## Security / Disclosure + If you find any bug that may be a security problem, please follow our instructions at [in our security policy](https://github.com/openmcp-project/openmcp-testing/security/policy) on how to report it. Please do not create GitHub issues for security-related doubts or problems. ## Code of Conduct diff --git a/e2e/domainobjects/dummy-cm.yaml b/e2e/domainobjects/dummy-cm.yaml new file mode 100644 index 0000000..5cc1e47 --- /dev/null +++ b/e2e/domainobjects/dummy-cm.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dummy +data: + foo: bar diff --git a/e2e/main_test.go b/e2e/main_test.go new file mode 100644 index 0000000..f649404 --- /dev/null +++ b/e2e/main_test.go @@ -0,0 +1,62 @@ +package e2e + +import ( + "flag" + "fmt" + "os" + "testing" + "time" + + "github.com/openmcp-project/openmcp-testing/pkg/providers" + "github.com/openmcp-project/openmcp-testing/pkg/setup" + "k8s.io/klog/v2" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/pkg/env" + "sigs.k8s.io/e2e-framework/pkg/envconf" +) + +var testenv env.Environment + +func TestMain(m *testing.M) { + initLogging() + openmcp := setup.OpenMCPSetup{ + Namespace: "openmcp-system", + Operator: setup.OpenMCPOperatorSetup{ + Name: "openmcp-operator", + Image: "ghcr.io/openmcp-project/images/openmcp-operator:v0.13.0", + Environment: "debug", + PlatformName: "platform", + }, + ClusterProviders: []providers.ClusterProviderSetup{ + { + Name: "kind", + Image: "ghcr.io/openmcp-project/images/cluster-provider-kind:v0.0.15", + Opts: []wait.Option{ + wait.WithTimeout(time.Minute), + }, + }, + }, + ServiceProviders: []providers.ServiceProviderSetup{ + { + Name: "crossplane", + Image: "ghcr.io/openmcp-project/images/service-provider-crossplane:v0.0.4", + Opts: []wait.Option{ + wait.WithTimeout(time.Minute), + }, + }, + }, + } + testenv = env.NewWithConfig(envconf.New().WithNamespace(openmcp.Namespace)) + if err := openmcp.Bootstrap(testenv); err != nil { + panic(fmt.Errorf("openmcp bootstrap failed: %v", err)) + } + os.Exit(testenv.Run(m)) +} + +func initLogging() { + klog.InitFlags(nil) + if err := flag.Set("v", "2"); err != nil { + panic(err) + } + flag.Parse() +} diff --git a/e2e/serviceprovider_test.go b/e2e/serviceprovider_test.go new file mode 100644 index 0000000..a8b7b39 --- /dev/null +++ b/e2e/serviceprovider_test.go @@ -0,0 +1,53 @@ +package e2e + +import ( + "context" + "testing" + "time" + + "github.com/openmcp-project/openmcp-testing/pkg/clusterutils" + "github.com/openmcp-project/openmcp-testing/pkg/providers" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +func TestServiceProvider(t *testing.T) { + basicProviderTest := features.New("provider test"). + Setup(providers.CreateMCP("test-mcp", wait.WithTimeout(2*time.Minute))). + Setup(providers.ImportServiceProviderAPIs("serviceproviderobjects", wait.WithTimeout(time.Minute))). + Setup(providers.ImportDomainAPIs("domainobjects", wait.WithTimeout(time.Minute))). + Assess("verify onboarding cluster objects", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + cfg, err := clusterutils.OnboardingConfig() + if err != nil { + t.Error(err) + return ctx + } + assertDummyConfigMap(ctx, t, cfg) + return ctx + }). + Assess("verify mcp cluster objects", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + cfg, err := clusterutils.McpConfig() + if err != nil { + t.Error(err) + return ctx + } + assertDummyConfigMap(ctx, t, cfg) + return ctx + }). + Teardown(providers.DeleteMCP("test-mcp", wait.WithTimeout(time.Minute))) + testenv.Test(t, basicProviderTest.Feature()) +} + +func assertDummyConfigMap(ctx context.Context, t *testing.T, cfg *envconf.Config) { + cm := &corev1.ConfigMap{} + if err := cfg.Client().Resources().Get(ctx, "dummy", corev1.NamespaceDefault, cm); err != nil { + t.Error(err) + return + } + v, ok := cm.Data["foo"] + if !ok || v != "bar" { + t.Errorf("expected foo:bar; got: %t %v", ok, v) + } +} diff --git a/e2e/serviceproviderobjects/dummy-cm.yaml b/e2e/serviceproviderobjects/dummy-cm.yaml new file mode 100644 index 0000000..5cc1e47 --- /dev/null +++ b/e2e/serviceproviderobjects/dummy-cm.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dummy +data: + foo: bar diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..75d7216 --- /dev/null +++ b/go.mod @@ -0,0 +1,78 @@ +module github.com/openmcp-project/openmcp-testing + +go 1.25.2 + +require ( + k8s.io/api v0.32.1 + k8s.io/apimachinery v0.32.1 + k8s.io/client-go v0.32.1 + k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/e2e-framework v0.6.0 +) + +require ( + al.essio.dev/pkg/shellescape v1.5.1 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/vladimirvivien/gexe v0.4.1 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.7.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/component-base v0.32.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/controller-runtime v0.20.0 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/kind v0.30.0 + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..93e0d95 --- /dev/null +++ b/go.sum @@ -0,0 +1,219 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vladimirvivien/gexe v0.4.1 h1:W9gWkp8vSPjDoXDu04Yp4KljpVMaSt8IQuHswLDd5LY= +github.com/vladimirvivien/gexe v0.4.1/go.mod h1:3gjgTqE2c0VyHnU5UOIwk7gyNzZDGulPb/DJPgcw64E= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= +k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.0 h1:jjkMo29xEXH+02Md9qaVXfEIaMESSpy3TBWPrsfQkQs= +sigs.k8s.io/controller-runtime v0.20.0/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= +sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM= +sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kind v0.30.0 h1:2Xi1KFEfSMm0XDcvKnUt15ZfgRPCT0OnCBbpgh8DztY= +sigs.k8s.io/kind v0.30.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/util.go b/internal/util.go new file mode 100644 index 0000000..ebfa5fe --- /dev/null +++ b/internal/util.go @@ -0,0 +1,68 @@ +package internal + +import ( + "io" + "os" + "strings" + "text/template" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/e2e-framework/klient/k8s" +) + +// ExecTemplate parses and executes textTemplate with the provided data +func ExecTemplate(textTemplate string, data interface{}) (string, error) { + tmpl, err := template.New("t").Parse(textTemplate) + if err != nil { + return "", err + } + result := strings.Builder{} + if err := tmpl.Execute(&result, data); err != nil { + return "", err + } + return result.String(), nil +} + +// ExecTemplateFile parses and executes a template referenced by a file with the provided data +func ExecTemplateFile(filePath string, data interface{}) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + bytes, err := io.ReadAll(f) + if err != nil { + return "", err + } + return ExecTemplate(string(bytes), data) +} + +// ToUnstructured converts a Object to Unstructured +func ToUnstructured(obj k8s.Object) (*unstructured.Unstructured, error) { + u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + return &unstructured.Unstructured{ + Object: u, + }, nil +} + +// IgnoreNotFound returns returns no error for IsNotFound +func IgnoreNotFound(err error) error { + if errors.IsNotFound(err) { + return nil + } + return err +} + +// UnstructuredRef returns an empty object with its identifying properties set +func UnstructuredRef(name string, namespace string, gvk schema.GroupVersionKind) *unstructured.Unstructured { + obj := &unstructured.Unstructured{} + obj.SetName(name) + obj.SetNamespace(namespace) + obj.SetGroupVersionKind(gvk) + return obj +} diff --git a/pkg/clusterutils/clusterutils.go b/pkg/clusterutils/clusterutils.go new file mode 100644 index 0000000..4f04a66 --- /dev/null +++ b/pkg/clusterutils/clusterutils.go @@ -0,0 +1,100 @@ +package clusterutils + +import ( + "context" + "fmt" + "strings" + + "github.com/openmcp-project/openmcp-testing/pkg/resources" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/e2e-framework/klient" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/klient/wait/conditions" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/kind/pkg/cluster" +) + +// ConfigByPrefix returns an environment Config with the passed in namespace and +// a klient that is set up to interact with the cluster identified by the passed +// in cluster name prefix +func ConfigByPrefix(prefix string, namespace string) (*envconf.Config, error) { + kind := cluster.NewProvider() + clusterName, err := retrieveKindClusterNameByPrefix(prefix) + if err != nil { + return nil, err + } + kubeConfig, err := kind.KubeConfig(clusterName, false) + if err != nil { + return nil, err + } + restConfig, err := clientcmd.RESTConfigFromKubeConfig([]byte(kubeConfig)) + if err != nil { + return nil, err + } + onboardingClient, err := klient.New(restConfig) + if err != nil { + return nil, err + } + return envconf.New().WithClient(onboardingClient).WithNamespace(namespace), nil +} + +// OnboardingConfig is a utility function to return an environment config to work +// with the onboarding cluster and default namespace +// In scenarios where you work with multiple onboarding clusters, use ConfigByPrefix instead +func OnboardingConfig() (*envconf.Config, error) { + return ConfigByPrefix("onboarding", corev1.NamespaceDefault) +} + +// McpConfig is a utility function to return an environment config to work +// with the mcp cluster and default namespace. +// In scenarios where you work with multiple MCPs, use ConfigByPrefix instead +func McpConfig() (*envconf.Config, error) { + return ConfigByPrefix("mcp", corev1.NamespaceDefault) +} + +func retrieveKindClusterNameByPrefix(prefix string) (string, error) { + kind := cluster.NewProvider() + clusters, err := kind.List() + if err != nil { + return "", err + } + for _, clusterName := range clusters { + if strings.HasPrefix(clusterName, prefix) { + return clusterName, nil + } + } + return "", fmt.Errorf("no cluster found with prefix %s", prefix) +} + +// ImportToOnboardingCluster applies a set of resources from a directory to the onboarding cluster +func ImportToOnboardingCluster(ctx context.Context, dir string, options ...wait.Option) (*unstructured.UnstructuredList, error) { + c, err := OnboardingConfig() + if err != nil { + return nil, fmt.Errorf("failed to retrieve onboarding cluster config: %v", err) + } + return importFromDir(ctx, c, dir, options...) +} + +// ImportToMcpCluster applies a set of resources from a directory to the mcp cluster +func ImportToMcpCluster(ctx context.Context, dir string, options ...wait.Option) (*unstructured.UnstructuredList, error) { + c, err := McpConfig() + if err != nil { + return nil, fmt.Errorf("failed to retrieve mcp cluster config: %v", err) + } + return importFromDir(ctx, c, dir, options...) +} + +func importFromDir(ctx context.Context, c *envconf.Config, dir string, options ...wait.Option) (*unstructured.UnstructuredList, error) { + objList, err := resources.CreateObjectsFromDir(ctx, c, dir) + if err != nil { + return nil, fmt.Errorf("failed to create objects from %s: %v", dir, err) + } + if options != nil { + if err := wait.For(conditions.New(c.Client().Resources()).ResourcesFound(objList), options...); err != nil { + return nil, err + } + } + return objList, nil +} diff --git a/pkg/conditions/conditions.go b/pkg/conditions/conditions.go new file mode 100644 index 0000000..6597fbc --- /dev/null +++ b/pkg/conditions/conditions.go @@ -0,0 +1,105 @@ +package conditions + +import ( + "context" + "fmt" + + "github.com/openmcp-project/openmcp-testing/internal" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog/v2" + "sigs.k8s.io/e2e-framework/klient/k8s" + "sigs.k8s.io/e2e-framework/pkg/envconf" +) + +// Match returns true if the conditionType of an object matches the conditionStatus. +// If an object is not found, the condition is not satisfied and no error is returned. +func Match(obj k8s.Object, cfg *envconf.Config, conditionType string, conditionStatus corev1.ConditionStatus) wait.ConditionWithContextFunc { + return func(ctx context.Context) (done bool, err error) { + klog.Infof("%s: waiting for condition %s %s", fmtObj(obj), conditionType, conditionStatus) + err = cfg.Client().Resources().Get(ctx, obj.GetName(), obj.GetNamespace(), obj) + if err != nil { + return false, internal.IgnoreNotFound(err) + } + return checkCondition(obj, conditionType, conditionStatus), nil + } +} + +// MatchList does the same as Match but for each object of a ObjectList +func MatchList(obj *unstructured.UnstructuredList, cfg *envconf.Config, conditionType string, conditionStatus corev1.ConditionStatus) wait.ConditionWithContextFunc { + return func(ctx context.Context) (done bool, err error) { + err = cfg.Client().Resources().List(ctx, obj) + if err != nil { + return false, internal.IgnoreNotFound(err) + } + for _, o := range obj.Items { + if !checkCondition(&o, conditionType, conditionStatus) { + return false, nil + } + } + // all objects match + return true, nil + } +} + +// Status returns true if the status key of an object matches the status value. +// If an object is not found, the condition is not satisfied and no error is returned. +func Status(obj k8s.Object, cfg *envconf.Config, key string, value string) wait.ConditionWithContextFunc { + return func(ctx context.Context) (done bool, err error) { + klog.Infof("%s: waiting for status %s %s", fmtObj(obj), key, value) + err = cfg.Client().Resources().Get(ctx, obj.GetName(), obj.GetNamespace(), obj) + if err != nil { + return false, internal.IgnoreNotFound(err) + } + u, err := internal.ToUnstructured(obj) + if err != nil { + return false, err + } + status, found, err := unstructured.NestedMap(u.Object, "status") + if err != nil { + return false, err + } + if !found { + return false, nil + } + return status[key] == value, nil + } +} + +func checkCondition(k8sobj k8s.Object, desiredType string, desiredStatus corev1.ConditionStatus) bool { + fmtobj := fmtObj(k8sobj) + u, err := internal.ToUnstructured(k8sobj) + if err != nil { + klog.Infof("%s: failed to convert object %v", fmtobj, err) + return false + } + conditions, ok, err := unstructured.NestedSlice(u.UnstructuredContent(), "status", "conditions") + if err != nil { + klog.Infof("%s: failed to extract conditions %v", fmtobj, err) + return false + } else if !ok { + klog.Infof("%s: does not have any conditions", fmtobj) + return false + } + status := "" + message := "" + for _, condition := range conditions { + c := condition.(map[string]interface{}) + curType := c["type"] + if curType == desiredType { + status = c["status"].(string) + msg, convertible := c["message"].(string) + if convertible { + message = msg + } + } + } + matchedConditionStatus := status == string(desiredStatus) + klog.Infof("%s condition %s: %s, message: %s", fmtobj, desiredType, status, message) + return matchedConditionStatus +} + +func fmtObj(obj k8s.Object) string { + return fmt.Sprintf("Object (%s) %s/%s", obj.GetObjectKind().GroupVersionKind(), obj.GetNamespace(), obj.GetName()) +} diff --git a/pkg/providers/clusterprovider.go b/pkg/providers/clusterprovider.go new file mode 100644 index 0000000..300327e --- /dev/null +++ b/pkg/providers/clusterprovider.go @@ -0,0 +1,163 @@ +package providers + +import ( + "context" + "testing" + + "github.com/openmcp-project/openmcp-testing/internal" + "github.com/openmcp-project/openmcp-testing/pkg/clusterutils" + "github.com/openmcp-project/openmcp-testing/pkg/conditions" + "github.com/openmcp-project/openmcp-testing/pkg/resources" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +const clusterProviderTemplate = ` +apiVersion: openmcp.cloud/v1alpha1 +kind: ClusterProvider +metadata: + name: {{.Name}} +spec: + image: {{.Image}} + extraVolumeMounts: + - mountPath: /var/run/docker.sock + name: docker + extraVolumes: + - name: docker + hostPath: + path: /var/run/host-docker.sock + type: Socket +` + +const mcpTemplate = ` +apiVersion: core.openmcp.cloud/v2alpha1 +kind: ManagedControlPlaneV2 +metadata: + name: {{.Name}} +spec: + iam: {} +` + +// ClusterProviderSetup represents the configuration parameters to set up a cluster provider +type ClusterProviderSetup struct { + Name string + Image string + Opts []wait.Option +} + +func mcpRef(ref types.NamespacedName) *unstructured.Unstructured { + return internal.UnstructuredRef(ref.Name, ref.Namespace, schema.GroupVersionKind{ + Group: "core.openmcp.cloud", + Version: "v2alpha1", + Kind: "managedcontrolplanev2", + }) +} + +func clusterRefList() *unstructured.UnstructuredList { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "clusters.openmcp.cloud", + Version: "v1alpha1", + Kind: "cluster", + }) + return list +} + +func clusterRef(ref types.NamespacedName) *unstructured.Unstructured { + return internal.UnstructuredRef(ref.Name, ref.Namespace, schema.GroupVersionKind{ + Group: "clusters.openmcp.cloud", + Version: "v1alpha1", + Kind: "cluster", + }) +} + +func clusterProviderRef(name string) *unstructured.Unstructured { + return internal.UnstructuredRef(name, "", schema.GroupVersionKind{ + Group: "openmcp.cloud", + Version: "v1alpha1", + Kind: "clusterprovider", + }) +} + +// InstallClusterProvider creates a cluster provider object on the platform cluster and waits until it is ready +func InstallClusterProvider(ctx context.Context, c *envconf.Config, clusterProvider ClusterProviderSetup) error { + klog.Infof("create cluster provider %s", clusterProvider.Name) + obj, err := resources.CreateObjectFromTemplate(ctx, c, clusterProviderTemplate, clusterProvider) + if err != nil { + return err + } + return wait.For(conditions.Match(obj, c, "Ready", corev1.ConditionTrue), clusterProvider.Opts...) +} + +// DeleteClusterProvider deletes the cluster provider object and waits until the object has been deleted +func DeleteClusterProvider(ctx context.Context, c *envconf.Config, name string, opts ...wait.Option) error { + klog.Infof("delete cluster provider: %s", name) + return resources.DeleteObject(ctx, c, clusterProviderRef(name), opts...) +} + +// CreateMCP creates an MCP object on the onboarding cluster and waits until it is ready +func CreateMCP(name string, opts ...wait.Option) features.Func { + return func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + klog.Infof("create MCP: %s", name) + onboardingCfg, err := clusterutils.OnboardingConfig() + if err != nil { + t.Error(err) + return ctx + } + obj, err := resources.CreateObjectFromTemplate(ctx, onboardingCfg, mcpTemplate, struct{ Name string }{Name: name}) + if err != nil { + t.Errorf("failed to create MCP: %v", err) + return ctx + } + if err := wait.For(conditions.Status(obj, onboardingCfg, "phase", "Ready"), opts...); err != nil { + t.Errorf("MCP failed to get ready: %v", err) + } + if err := ClustersReady(ctx, c, opts...); err != nil { + t.Errorf("MCP cluster failed to get ready: %v", err) + } + return ctx + } +} + +// DeleteMCP deletes the MCP object on the onboarding cluster and waits until the object has been deleted +func DeleteMCP(name string, opts ...wait.Option) features.Func { + return func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + klog.Infof("delete MCP: %s", name) + onboardingCfg, err := clusterutils.OnboardingConfig() + if err != nil { + t.Error(err) + return ctx + } + mcp := mcpRef(types.NamespacedName{ + Namespace: corev1.NamespaceDefault, + Name: name, + }) + err = resources.DeleteObject(ctx, onboardingCfg, mcp, opts...) + if err != nil { + t.Errorf("failed to delete MCP %s: %v", name, err) + return ctx + } + return ctx + } +} + +// ClustersReady returns true if all cluster objects are ready +func ClustersReady(ctx context.Context, c *envconf.Config, options ...wait.Option) error { + if err := wait.For(conditions.MatchList(clusterRefList(), c, "Ready", corev1.ConditionTrue), options...); err != nil { + return err + } + klog.Infof("all clusters ready") + return nil +} + +// DeleteCluster deletes the referenced cluster object +func DeleteCluster(ctx context.Context, c *envconf.Config, ref types.NamespacedName, options ...wait.Option) error { + klog.Infof("delete cluster: %s", ref) + return resources.DeleteObject(ctx, c, clusterRef(ref), options...) +} diff --git a/pkg/providers/serviceprovider.go b/pkg/providers/serviceprovider.go new file mode 100644 index 0000000..4ae5860 --- /dev/null +++ b/pkg/providers/serviceprovider.go @@ -0,0 +1,84 @@ +package providers + +import ( + "context" + "testing" + + "github.com/openmcp-project/openmcp-testing/pkg/clusterutils" + "github.com/openmcp-project/openmcp-testing/pkg/conditions" + "github.com/openmcp-project/openmcp-testing/pkg/resources" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +const serviceProviderTemplate = ` +apiVersion: openmcp.cloud/v1alpha1 +kind: ServiceProvider +metadata: + name: {{.Name}} +spec: + image: {{.Image}} +` + +// ServiceProviderSetup represents the configuration parameters to set up a service provider +type ServiceProviderSetup struct { + Name string + Image string + Opts []wait.Option +} + +func serviceProviderRef(name string) *unstructured.Unstructured { + obj := &unstructured.Unstructured{} + obj.SetName(name) + obj.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "openmcp.cloud", + Version: "v1alpha1", + Kind: "ServiceProvider", + }) + return obj +} + +// InstallServiceProvider creates a service provider object on the platform cluster and waits until it is ready +func InstallServiceProvider(ctx context.Context, c *envconf.Config, sp ServiceProviderSetup) error { + klog.Infof("create service provider: %s", sp.Name) + obj, err := resources.CreateObjectFromTemplate(ctx, c, serviceProviderTemplate, sp) + if err != nil { + return err + } + return wait.For(conditions.Match(obj, c, "Ready", corev1.ConditionTrue), sp.Opts...) +} + +// ImportServiceProviderAPIs iterates over each resource from the passed in directory +// and applies it to the onboarding cluster +func ImportServiceProviderAPIs(directory string, opts ...wait.Option) features.Func { + return func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + klog.Infof("apply service provider resources to onboarding cluster from %s ...", directory) + if _, err := clusterutils.ImportToOnboardingCluster(ctx, directory, opts...); err != nil { + t.Error(err) + } + return ctx + } +} + +// ImportDomainAPIs iterates over each resource from the passed in directory +// and applies it to a MCP cluster +func ImportDomainAPIs(directory string, opts ...wait.Option) features.Func { + return func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + klog.Infof("apply service provider resources to MCP cluster from %s ...", directory) + if _, err := clusterutils.ImportToMcpCluster(ctx, directory, opts...); err != nil { + t.Error(err) + } + return ctx + } +} + +// DeleteServiceProvider deletes the service provider object on the platform cluster and waits until the object has been deleted +func DeleteServiceProvider(ctx context.Context, c *envconf.Config, name string, opts ...wait.Option) error { + klog.Infof("delete service provider: %s", name) + return resources.DeleteObject(ctx, c, serviceProviderRef(name), opts...) +} diff --git a/pkg/resources/resources.go b/pkg/resources/resources.go new file mode 100644 index 0000000..672175a --- /dev/null +++ b/pkg/resources/resources.go @@ -0,0 +1,88 @@ +package resources + +import ( + "context" + "os" + "strings" + + "github.com/openmcp-project/openmcp-testing/internal" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/klog/v2" + "sigs.k8s.io/e2e-framework/klient/decoder" + "sigs.k8s.io/e2e-framework/klient/k8s" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/klient/wait/conditions" + "sigs.k8s.io/e2e-framework/pkg/envconf" +) + +// DeleteObject deletes the passed in object if it exists +func DeleteObject(ctx context.Context, c *envconf.Config, obj k8s.Object, options ...wait.Option) error { + err := c.Client().Resources().Get(ctx, obj.GetName(), obj.GetNamespace(), obj) + if err != nil { + return internal.IgnoreNotFound(err) + } + if err = c.Client().Resources().Delete(ctx, obj); err != nil { + return internal.IgnoreNotFound(err) + } + if options != nil { + return wait.For(conditions.New(c.Client().Resources()).ResourceDeleted(obj), options...) + } + return nil +} + +// CreateObjectsFromTemplateFile creates objects by first applying the passed data to a template file on the file system +func CreateObjectsFromTemplateFile(ctx context.Context, cfg *envconf.Config, filePath string, data interface{}) (*unstructured.UnstructuredList, error) { + manifest, err := internal.ExecTemplateFile(filePath, data) + if err != nil { + return nil, err + } + return createObjectsFromManifest(ctx, cfg, manifest) +} + +// CreateObjectFromTemplate creates a single object by first applying the passed in data to a template +func CreateObjectFromTemplate(ctx context.Context, cfg *envconf.Config, template string, data interface{}) (*unstructured.Unstructured, error) { + manifest, err := internal.ExecTemplate(template, data) + if err != nil { + return nil, err + } + obj := &unstructured.Unstructured{} + err = decoder.DecodeString(manifest, obj, decoder.MutateNamespace(cfg.Namespace())) + if err != nil { + return nil, err + } + err = cfg.Client().Resources().Create(ctx, obj) + if err != nil { + return nil, err + } + return obj, nil +} + +func createObjectsFromManifest(ctx context.Context, cfg *envconf.Config, manifest string) (*unstructured.UnstructuredList, error) { + r := strings.NewReader(manifest) + list := &unstructured.UnstructuredList{} + err := decoder.DecodeEach(ctx, r, + func(ctx context.Context, obj k8s.Object) error { + return createAndPopulateList(ctx, obj, list, cfg) + }, decoder.MutateNamespace(cfg.Namespace())) + return list, err +} + +// CreateObjectsFromDir creates objects specified by a file on the file system +func CreateObjectsFromDir(ctx context.Context, cfg *envconf.Config, dir string) (*unstructured.UnstructuredList, error) { + list := &unstructured.UnstructuredList{} + err := decoder.DecodeEachFile(ctx, os.DirFS(dir), "*", + func(ctx context.Context, obj k8s.Object) error { + return createAndPopulateList(ctx, obj, list, cfg) + }, decoder.MutateNamespace(cfg.Namespace())) + return list, err +} + +func createAndPopulateList(ctx context.Context, obj k8s.Object, list *unstructured.UnstructuredList, cfg *envconf.Config) error { + u, err := internal.ToUnstructured(obj) + if err != nil { + return err + } + list.Items = append(list.Items, *u) + klog.Infof("creating object (%s) %s/%s", obj.GetObjectKind().GroupVersionKind(), obj.GetNamespace(), obj.GetName()) + return decoder.CreateIgnoreAlreadyExists(cfg.Client().Resources())(ctx, obj) +} diff --git a/pkg/setup/bootstrap.go b/pkg/setup/bootstrap.go new file mode 100644 index 0000000..3df756a --- /dev/null +++ b/pkg/setup/bootstrap.go @@ -0,0 +1,121 @@ +package setup + +import ( + "context" + "time" + + "github.com/openmcp-project/openmcp-testing/pkg/providers" + "github.com/openmcp-project/openmcp-testing/pkg/resources" + apimachinerytypes "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/klient/wait/conditions" + "sigs.k8s.io/e2e-framework/pkg/env" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/envfuncs" + "sigs.k8s.io/e2e-framework/pkg/types" + "sigs.k8s.io/e2e-framework/support/kind" +) + +type OpenMCPSetup struct { + Namespace string + Operator OpenMCPOperatorSetup + ClusterProviders []providers.ClusterProviderSetup + ServiceProviders []providers.ServiceProviderSetup +} + +type OpenMCPOperatorSetup struct { + Name string + Namespace string + Image string + Environment string + PlatformName string +} + +// Bootstrap sets up a the minimum set of components of an openMCP installation +func (s *OpenMCPSetup) Bootstrap(testenv env.Environment) error { + platformClusterName := envconf.RandomName("platform-cluster", 16) + s.Operator.Namespace = s.Namespace + testenv.Setup(createPlatformCluster(platformClusterName)). + Setup(envfuncs.CreateNamespace(s.Namespace)). + Setup(s.installOpenMCPOperator()). + Setup(s.installClusterProviders()). + Setup(s.installServiceProviders()). + Setup(s.verifyEnvironment()). + Finish(s.cleanup()). + Finish(envfuncs.DestroyCluster(platformClusterName)) + return nil +} + +func createPlatformCluster(name string) types.EnvFunc { + klog.Info("create platform cluster...") + return envfuncs.CreateClusterWithConfig(kind.NewProvider(), name, "../pkg/setup/kind/config.yaml") +} + +func (s *OpenMCPSetup) cleanup() types.EnvFunc { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + klog.Info("cleaning up environment...") + for _, sp := range s.ServiceProviders { + if err := providers.DeleteServiceProvider(ctx, c, sp.Name, wait.WithTimeout(time.Minute)); err != nil { + klog.Errorf("delete service provider failed: %v", err) + } + } + if err := providers.DeleteCluster(ctx, c, apimachinerytypes.NamespacedName{Namespace: s.Namespace, Name: "onboarding"}, + wait.WithTimeout(time.Second*20)); err != nil { + klog.Errorf("delete cluster failed: %v", err) + } + for _, cp := range s.ClusterProviders { + if err := providers.DeleteClusterProvider(ctx, c, cp.Name, wait.WithTimeout(time.Minute)); err != nil { + klog.Errorf("delete cluster provider failed: %v", err) + } + } + return ctx, nil + } +} + +func (s *OpenMCPSetup) verifyEnvironment() types.EnvFunc { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + klog.Info("verify environment...") + return ctx, providers.ClustersReady(ctx, c, wait.WithTimeout(time.Minute)) + } +} + +func (s *OpenMCPSetup) installOpenMCPOperator() types.EnvFunc { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + // apply openmcp operator manifests + if _, err := resources.CreateObjectsFromTemplateFile(ctx, c, "../pkg/setup/templates/openmcp-operator.yaml", s.Operator); err != nil { + return ctx, err + } + // wait for deployment to be ready + if err := wait.For(conditions.New(c.Client().Resources()). + DeploymentAvailable(s.Operator.Name, s.Operator.Namespace), + wait.WithTimeout(time.Minute)); err != nil { + return ctx, err + } + klog.Info("openmcp operator ready") + return ctx, nil + } +} + +func (s *OpenMCPSetup) installClusterProviders() env.Func { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + for _, cp := range s.ClusterProviders { + if err := providers.InstallClusterProvider(ctx, c, cp); err != nil { + return ctx, err + } + } + return ctx, nil + } +} + +// InstallServiceProvider creates a service provider object on the platform cluster and waits until it is ready +func (s *OpenMCPSetup) installServiceProviders() env.Func { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + for _, sp := range s.ServiceProviders { + if err := providers.InstallServiceProvider(ctx, c, sp); err != nil { + return ctx, err + } + } + return ctx, nil + } +} diff --git a/pkg/setup/kind/config.yaml b/pkg/setup/kind/config.yaml new file mode 100644 index 0000000..3bcdfea --- /dev/null +++ b/pkg/setup/kind/config.yaml @@ -0,0 +1,7 @@ +apiVersion: kind.x-k8s.io/v1alpha4 +kind: Cluster +nodes: + - role: control-plane + extraMounts: + - hostPath: /var/run/docker.sock + containerPath: /var/run/host-docker.sock diff --git a/pkg/setup/templates/openmcp-operator.yaml b/pkg/setup/templates/openmcp-operator.yaml new file mode 100644 index 0000000..7b79cc7 --- /dev/null +++ b/pkg/setup/templates/openmcp-operator.yaml @@ -0,0 +1,137 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{.Name}} + namespace: {{.Namespace}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{.Name}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: {{.Name}} + namespace: {{.Namespace}} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{.Name}} + namespace: {{.Namespace}} +data: + config: | + managedControlPlane: + mcpClusterPurpose: mcp + scheduler: + scope: Cluster + purposeMappings: + mcp: + template: + spec: + profile: kind + tenancy: Exclusive + platform: + template: + spec: + profile: kind + tenancy: Shared + onboarding: + template: + spec: + profile: kind + tenancy: Shared +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.Name}} + namespace: {{.Namespace}} +spec: + replicas: 1 + selector: + matchLabels: + app: {{.Name}} + template: + metadata: + labels: + app: {{.Name}} + spec: + serviceAccountName: {{.Name}} + initContainers: + - image: {{.Image}} + name: openmcp-operator-init + resources: {} + args: + - init + - --environment + - {{.Environment}} + - --config + - /etc/openmcp-operator/config + env: + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + - name: POD_SERVICE_ACCOUNT_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.serviceAccountName + volumeMounts: + - name: config + mountPath: /etc/openmcp-operator + readOnly: true + containers: + - image: {{.Image}} + name: {{.Name}} + resources: {} + args: + - run + - --environment + - {{.Environment}} + - --config + - /etc/openmcp-operator/config + env: + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + - name: POD_SERVICE_ACCOUNT_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.serviceAccountName + volumeMounts: + - name: config + mountPath: /etc/openmcp-operator + readOnly: true + volumes: + - name: config + configMap: + name: {{.Name}}