From 96da03dfd3a59db69591ebd93a390551ba952798 Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Mon, 15 Apr 2024 09:17:01 -0700 Subject: [PATCH 01/26] use exp version --- .github/version/versions.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/version/versions.txt b/.github/version/versions.txt index 76be08692..ecc7f4ffb 100644 --- a/.github/version/versions.txt +++ b/.github/version/versions.txt @@ -1 +1 @@ -0.48.21 +0.48-exp.21 From c99c1d49400d4ad09fb0e9e3cf123d96ae893270 Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Mon, 15 Apr 2024 11:04:59 -0700 Subject: [PATCH 02/26] go mod tidy --- cli/go.mod | 8 +++++--- cli/go.sum | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index f3dd4b90c..5f34a0e03 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -9,7 +9,6 @@ replace github.com/eclipse-symphony/symphony/coa => ../coa require github.com/spf13/cobra v1.6.1 require ( - github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/eclipse-symphony/symphony/coa v0.0.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect @@ -20,7 +19,10 @@ require ( helm.sh/helm/v3 v3.10.0 // indirect ) -require github.com/princjef/mageutil v1.0.0 +require ( + github.com/cenkalti/backoff/v4 v4.2.1 + github.com/princjef/mageutil v1.0.0 +) require ( github.com/eclipse-symphony/symphony/api v0.0.0 @@ -29,6 +31,6 @@ require ( github.com/mattn/go-runewidth v0.0.13 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/sys v0.17.0 // indirect sigs.k8s.io/yaml v1.3.0 ) diff --git a/cli/go.sum b/cli/go.sum index b314e8347..7e790b1ca 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -49,7 +49,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI= golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -61,8 +61,8 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/VividCortex/ewma.v1 v1.1.1/go.mod h1:TekXuFipeiHWiAlO1+wSS23vTcyFau5u3rxXUSXj710= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 4a96738a56b4a1a0431adb59a0e181a9ef2536a0 Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Thu, 18 Apr 2024 08:50:18 -0700 Subject: [PATCH 03/26] commit to experimental branch --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4f73aa6a6..05f08002f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -151,7 +151,7 @@ jobs: uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} - branch: main + branch: experimental - name: Create Release id: create_release From b51cf99b5ad50339c1243edf56fa9c55fda09a03 Mon Sep 17 00:00:00 2001 From: Jiawei Du <59427055+msftcoderdjw@users.noreply.github.com> Date: Fri, 19 Apr 2024 04:10:25 +0800 Subject: [PATCH 04/26] fix update target result (#227) --- api/pkg/apis/v1alpha1/model/summary.go | 1 + api/pkg/apis/v1alpha1/model/summary_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/api/pkg/apis/v1alpha1/model/summary.go b/api/pkg/apis/v1alpha1/model/summary.go index b3de33ec7..b830bbed9 100644 --- a/api/pkg/apis/v1alpha1/model/summary.go +++ b/api/pkg/apis/v1alpha1/model/summary.go @@ -56,5 +56,6 @@ func (s *SummarySpec) UpdateTargetResult(target string, spec TargetResultSpec) { v.Status = status v.Message = message maps.Copy(v.ComponentResults, spec.ComponentResults) + s.TargetResults[target] = v } } diff --git a/api/pkg/apis/v1alpha1/model/summary_test.go b/api/pkg/apis/v1alpha1/model/summary_test.go index 718ca32e6..daceced92 100644 --- a/api/pkg/apis/v1alpha1/model/summary_test.go +++ b/api/pkg/apis/v1alpha1/model/summary_test.go @@ -29,5 +29,6 @@ func TestUpdateTargetResult(t *testing.T) { s.UpdateTargetResult("target2", TargetResultSpec{ Status: "ERROR", }) + assert.Equal(t, "ERROR", s.TargetResults["target2"].Status) assert.Equal(t, 0, s.SuccessCount) //ver 0.48.1: UpdateTargetResult no longer updates success count } From 90392462f0170818aa3ae0122eb3a2b5135c5c9e Mon Sep 17 00:00:00 2001 From: Jiawei Du <59427055+msftcoderdjw@users.noreply.github.com> Date: Sat, 20 Apr 2024 20:22:16 +0800 Subject: [PATCH 05/26] [API] Merge ADO changes to OSS (#220) * Merge ADO changes to OSS * refine k8s magefile * fix gatekeeper tests * export symphony logs in integration test * fix requeue when deployment is in-progress * Remove unnecessary isDeploymentFinished, use summaryResult.State to check * Remove unnecessary isDeploymentFinished, use summaryResult.State to check * Merge ADO changes to OSS * Remove unnecessary isDeploymentFinished, use summaryResult.State to check * pass context in apiclient call * addressing comments * adjust log level * fix typo and use time.Now().UTC() in metrics --- .github/workflows/go.yml | 32 +- .github/workflows/integration.yml | 10 + api/.dockerignore | 3 +- api/.gitignore | 4 +- api/constants/constants.go | 16 + api/go.mod | 74 ++- api/go.sum | 178 ++--- api/magefile.go | 2 +- .../catalogs/catalogs-manager_test.go | 6 + .../v1alpha1/managers/jobs/jobs-manager.go | 153 +++-- .../managers/solution/metrics/attributes.go | 18 + .../managers/solution/metrics/metrics.go | 62 ++ .../managers/solution/solution-manager.go | 109 +++- .../apis/v1alpha1/model/deployableStatus.go | 15 + api/pkg/apis/v1alpha1/model/deployment.go | 2 + api/pkg/apis/v1alpha1/model/instance.go | 8 +- api/pkg/apis/v1alpha1/model/plan.go | 3 +- api/pkg/apis/v1alpha1/model/plan_test.go | 5 +- api/pkg/apis/v1alpha1/model/summary.go | 20 +- api/pkg/apis/v1alpha1/model/summary_test.go | 30 + api/pkg/apis/v1alpha1/model/target.go | 8 +- api/pkg/apis/v1alpha1/model/utils.go | 10 + api/pkg/apis/v1alpha1/model/validationrule.go | 11 +- .../v1alpha1/model/validationrule_test.go | 90 +++ .../providers/providerfactory_test.go | 165 +++-- .../providers/target/configmap/configmap.go | 2 +- .../target/configmap/configmap_test.go | 61 +- .../target/conformance/target_conformance.go | 3 +- .../providers/target/docker/docker.go | 6 +- .../v1alpha1/providers/target/helm/auth.go | 127 ++++ .../v1alpha1/providers/target/helm/helm.go | 397 ++++++++--- .../providers/target/helm/helm_test.go | 343 +++++++--- .../providers/target/helm/postrenderer.go | 132 ++++ .../v1alpha1/providers/target/http/http.go | 9 +- .../providers/target/ingress/ingress.go | 2 +- .../apis/v1alpha1/providers/target/k8s/k8s.go | 109 ++-- .../providers/target/kubectl/kubectl.go | 580 +++++++++++++++-- .../providers/target/kubectl/kubectl_test.go | 217 ++++++- .../providers/target/metrics/attributes.go | 24 + .../providers/target/metrics/metrics.go | 131 ++++ .../v1alpha1/providers/target/mqtt/mqtt.go | 4 +- .../v1alpha1/providers/target/proxy/proxy.go | 4 +- .../providers/target/script/script.go | 83 ++- .../providers/target/staging/staging.go | 4 +- api/pkg/apis/v1alpha1/utils/apiclient.go | 614 ++++++++++++++++++ .../v1alpha1/utils/metahelper/metahelper.go | 90 +++ api/pkg/apis/v1alpha1/utils/symphony-api.go | 70 +- .../apis/v1alpha1/utils/symphony-api_test.go | 77 ++- api/pkg/apis/v1alpha1/utils/utils.go | 13 +- api/pkg/apis/v1alpha1/vendors/agent-vendor.go | 5 +- .../apis/v1alpha1/vendors/instances-vendor.go | 15 +- .../apis/v1alpha1/vendors/job-vendor_test.go | 3 + .../apis/v1alpha1/vendors/solution-vendor.go | 44 +- .../apis/v1alpha1/vendors/solutions-vendor.go | 4 +- .../apis/v1alpha1/vendors/targets-vendor.go | 11 +- api/symphony-k8s-proxy-mqtt.json | 2 +- api/symphony-powershell-over-mqtt.json | 2 +- api/symphony-script-proxy.json | 2 +- api/symphony-win-proxy.json | 2 +- .../apis/v1alpha2/bindings/http/metrics.go | 2 +- .../v1alpha2/observability/observability.go | 4 +- k8s/apis/ai/v1/webhook_suite_test.go | 11 +- k8s/apis/fabric/v1/webhook_suite_test.go | 11 +- k8s/apis/solution/v1/webhook_suite_test.go | 11 +- k8s/controllers/ai/suite_test.go | 12 +- k8s/controllers/fabric/suite_test.go | 12 +- k8s/controllers/fabric/target_controller.go | 10 +- .../solution/instance_controller.go | 10 +- k8s/controllers/solution/suite_test.go | 12 +- k8s/go.mod | 50 +- k8s/go.sum | 107 +-- k8s/magefile.go | 25 +- k8s/testing/spec_runner.go | 39 ++ k8s/utils/helper.go | 4 +- packages/mage/mage.go | 50 +- .../integration/scenarios/00.unit/magefile.go | 1 + .../scenarios/01.update/magefile.go | 1 + .../scenarios/02.basic/magefile.go | 1 + .../03.basicWithNsDelete/magefile.go | 1 + .../verify/manifest_test.go | 2 +- .../scenarios/04.workflow/magefile.go | 1 + .../scenarios/05.catalog/magefile.go | 1 + test/localenv/go.mod | 2 +- test/localenv/magefile.go | 80 ++- 84 files changed, 3862 insertions(+), 824 deletions(-) create mode 100644 api/pkg/apis/v1alpha1/managers/solution/metrics/attributes.go create mode 100644 api/pkg/apis/v1alpha1/managers/solution/metrics/metrics.go create mode 100644 api/pkg/apis/v1alpha1/model/deployableStatus.go create mode 100644 api/pkg/apis/v1alpha1/providers/target/helm/auth.go create mode 100644 api/pkg/apis/v1alpha1/providers/target/helm/postrenderer.go create mode 100644 api/pkg/apis/v1alpha1/providers/target/metrics/attributes.go create mode 100644 api/pkg/apis/v1alpha1/providers/target/metrics/metrics.go create mode 100644 api/pkg/apis/v1alpha1/utils/apiclient.go create mode 100644 api/pkg/apis/v1alpha1/utils/metahelper/metahelper.go create mode 100644 k8s/testing/spec_runner.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 56448b6cc..4ed182357 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -20,6 +20,9 @@ jobs: TEST_MQTT_LOCAL_ENABLED: yes TEST_DOCKER_ENABLED: yes TEST_K8S_STATE: yes + TEST_SYMPHONY_HELM_VERSION: yes + TEST_HELM_CHART: yes + TEST_CONFIGMAP: yes # requires minikube start steps: - uses: actions/checkout@v3 @@ -64,23 +67,34 @@ jobs: sudo service mosquitto start sudo service mosquitto status + - name: Install Mage + run: | + cd .. + git clone https://github.com/magefile/mage + cd mage + go run bootstrap.go + cd .. + - name: COA Test - run: cd coa && go test -race -v ./... + run: cd coa && mage cleanTest - name: API Build run: cd api && go build -o symphony-api - name: API Test run: | - echo "TEST_KUBECTL:$TEST_KUBECTL TEST_MINIKUBE_ENABLED:$TEST_MINIKUBE_ENABLED TEST_K8S_STATE: $TEST_K8S_STATE" + echo "TEST_KUBECTL:$TEST_KUBECTL TEST_MINIKUBE_ENABLED:$TEST_MINIKUBE_ENABLED TEST_K8S_STATE: $TEST_K8S_STATE TEST_CONFIGMAP: $TEST_CONFIGMAP" export REPOPATH="${{ github.workspace }}" echo "REPOPATH=$REPOPATH" - cd api && go test -race -v ./... -run '^[^C]*$|^[^c][^o]*$|^[^c][^o]*o[^n][^f][^o][^r][^m][^a][^n][^c][^e][^C]*$' + cd api && mage cleanTest + + - name: K8S Test + run: cd k8s && mage operatorTest - name: target-api-testcoverage-app run: | - cd api && go test -race -coverprofile=coverage.out ./... - COVERAGE=`go tool cover -func=coverage.out | grep total: | grep -Eo '[0-9]+\.[0-9]+'` + cd api + COVERAGE=`mage printCoverage` echo "coverage=$COVERAGE" go tool cover -html=coverage.out -o coverage-api.html continue-on-error: true @@ -88,8 +102,8 @@ jobs: - name: target-k8s-testcoverage-app run: | - cd k8s && go test -race -coverprofile=coverage.out ./... - COVERAGE=`go tool cover -func=coverage.out | grep total: | grep -Eo '[0-9]+\.[0-9]+'` + cd k8s + COVERAGE=`mage printCoverage` echo "coverage=$COVERAGE" go tool cover -html=coverage.out -o coverage-k8s.html continue-on-error: true @@ -97,8 +111,8 @@ jobs: - name: target-coa-testcoverage-app run: | - cd coa && go test -race -coverprofile=coverage.out ./... - COVERAGE=`go tool cover -func=coverage.out | grep total: | grep -Eo '[0-9]+\.[0-9]+'` + cd coa + COVERAGE=`mage printCoverage` echo "coverage=$COVERAGE" go tool cover -html=coverage.out -o coverage-coa.html continue-on-error: true diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index b63faf29e..35270cac7 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -110,6 +110,16 @@ jobs: run: | cd test/integration/scenarios/05.catalog/ && mage test + - name: Collect and upload symphony logs + uses: actions/upload-artifact@v2 + with: + name: symphony-logs + path: | + /tmp/symhony-integration-test-logs/**/*.log + continue-on-error: true + if: always() + + diff --git a/api/.dockerignore b/api/.dockerignore index 6dd29b7f8..cedde054f 100644 --- a/api/.dockerignore +++ b/api/.dockerignore @@ -1 +1,2 @@ -bin/ \ No newline at end of file +bin/ +.env.debug \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index fad0b1c2d..2c41c5566 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -18,4 +18,6 @@ bin/ symphony-api symphony-agent symphony-api-mac -symphony-api.exe \ No newline at end of file +symphony-api.exe +.env.debug +.vscode/serviceaccount/ diff --git a/api/constants/constants.go b/api/constants/constants.go index d919de54b..35d1ea168 100644 --- a/api/constants/constants.go +++ b/api/constants/constants.go @@ -31,4 +31,20 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE` + DefaultScope = "default" + SATokenPath = "/var/run/secrets/tokens/symphony-api-token" + // These constants need to be in a shared package. + GroupPrefix = "symphony" + ManagerMetaKey = GroupPrefix + "/managed-by" + InstanceMetaKey = GroupPrefix + "/instance" +) + +// Environment variables keys +const ( + SymphonyCertEnvName = "SYMPHONY_ROOT_CA" + SATokenPathName = "SA_TOKEN_PATH" + ApiCertEnvName = "API_SERVING_CA" + UseServiceAccountTokenEnvName = "USE_SERVICE_ACCOUNT_TOKENS" + SymphonyAPIUrlEnvName = "SYMPHONY_API_URL" + API = "symphony-api" ) diff --git a/api/go.mod b/api/go.mod index 18a77a744..c0e08e28b 100644 --- a/api/go.mod +++ b/api/go.mod @@ -8,11 +8,11 @@ replace github.com/eclipse-symphony/symphony/packages/mage => ../packages/mage require ( github.com/eclipse-symphony/symphony/coa v0.0.0 - github.com/spf13/cobra v1.5.0 + github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 github.com/valyala/fasthttp v1.50.0 - k8s.io/api v0.25.0 - k8s.io/apimachinery v0.25.0 + k8s.io/api v0.25.2 + k8s.io/apimachinery v0.25.2 k8s.io/client-go v0.25.0 ) @@ -20,24 +20,31 @@ require ( require ( github.com/eclipse-symphony/symphony/packages/mage v0.0.0-00010101000000-000000000000 github.com/eclipse/paho.mqtt.golang v1.4.2 + github.com/fsnotify/fsnotify v1.6.0 github.com/princjef/mageutil v1.0.0 golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 + helm.sh/helm/v3 v3.10.0 ) require ( code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect - github.com/Microsoft/go-winio v0.5.1 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/hcsshim v0.10.0-rc.7 // indirect github.com/VividCortex/ewma v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cheggaaa/pb/v3 v3.0.4 // indirect github.com/go-redis/redis/v7 v7.4.1 // indirect github.com/gofrs/uuid v3.3.0+incompatible // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/microsoft/ApplicationInsights-Go v0.4.4 // indirect + github.com/onsi/ginkgo/v2 v2.13.1 // indirect + github.com/onsi/gomega v1.29.0 // indirect github.com/openzipkin/zipkin-go v0.4.1 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 // indirect @@ -49,6 +56,8 @@ require ( go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/tools v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect ) @@ -61,22 +70,20 @@ require ( github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/Masterminds/sprig/v3 v3.2.2 // indirect github.com/Masterminds/squirrel v1.5.3 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/containerd v1.6.6 // indirect + github.com/containerd/containerd v1.7.0-beta.0 github.com/cyphar/filepath-securejoin v0.2.3 // indirect - github.com/docker/cli v20.10.17+incompatible // indirect - github.com/docker/distribution v2.8.1+incompatible // indirect - github.com/docker/docker v20.10.17+incompatible - github.com/docker/docker-credential-helpers v0.6.4 // indirect + github.com/docker/cli v24.0.6+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v20.10.24+incompatible + github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.10.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/color v1.13.0 // indirect @@ -84,7 +91,7 @@ require ( github.com/go-gorp/gorp/v3 v3.0.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.19.14 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/google/btree v1.0.1 // indirect @@ -104,29 +111,29 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect + github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/prometheus/client_golang v1.12.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect github.com/rubenv/sql-migrate v1.1.2 // indirect - github.com/russross/blackfriday v1.5.2 // indirect + github.com/russross/blackfriday v1.6.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.4.1 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.1.0 // indirect @@ -148,13 +155,13 @@ require ( ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fasthttp/router v1.4.12 // indirect + github.com/fasthttp/router v1.4.20 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 @@ -162,8 +169,8 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.4.0 - github.com/imdario/mergo v0.3.12 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.3 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -171,8 +178,8 @@ require ( github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect - github.com/sirupsen/logrus v1.8.1 // indirect + github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect @@ -191,8 +198,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - helm.sh/helm/v3 v3.10.0 - k8s.io/klog/v2 v2.70.1 // indirect - k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) diff --git a/api/go.sum b/api/go.sum index 2bfc44871..5eaf994d2 100644 --- a/api/go.sum +++ b/api/go.sum @@ -39,18 +39,20 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c h1:5eeuG0BHx1+DHeT3AP+ISKZ2ht1UjGhm581ljqYpVeQ= code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 h1:8kDqDngH+DmVBiCtIjCFTGa7MBnsIOkF9IccInFEbjk= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 h1:uqM+VoHjVH6zdlkLF2b6O0ZANcHoj3rO0PoQ3jglUJA= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2/go.mod h1:twTKAa1E6hLmSDjLhaCkbTMQKc7p/rNLU40rLxGEOCI= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 h1:LNHhpdK7hzUcx/k1LIcuh5k7k1LGIWLQfCjaneSj7Fc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -69,13 +71,10 @@ github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmy github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= -github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NECxo= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.10.0-rc.7 h1:HBytQPxcv8Oy4244zbQbe6hnOnx544eL5QPUqhJldz8= +github.com/Microsoft/hcsshim v0.10.0-rc.7/go.mod h1:ILuwjA+kNW+MrN/w5un7n3mTqkwsFu4Bp05/okFUZlE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= @@ -84,7 +83,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -123,47 +121,45 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= -github.com/containerd/containerd v1.6.6 h1:xJNPhbrmz8xAMDNoVjHy9YHtWwEQNS+CDkcIRh7t8Y0= -github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/containerd v1.7.0-beta.0 h1:TmelrlMneeWvAbqqTB9XQ3yCc3voPrBT/k80D8kj5dw= +github.com/containerd/containerd v1.7.0-beta.0/go.mod h1:d+x3kmR4hnXSGTCbLRpBFnP5lOEjqm7dLwZ4UCz01WI= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk= -github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= -github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= -github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= -github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= -github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY= +github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= +github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4= github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= -github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= -github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= +github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -175,16 +171,17 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= -github.com/fasthttp/router v1.4.12 h1:QEgK+UKARaC1bAzJgnIhdUMay6nwp+YFq6VGPlyKN1o= -github.com/fasthttp/router v1.4.12/go.mod h1:41Qdc4Z4T2pWVVtATHCnoUnOtxdBoeKEYJTXhHwbxCQ= +github.com/fasthttp/router v1.4.20 h1:yPeNxz5WxZGojzolKqiP15DTXnxZce9Drv577GBrDgU= +github.com/fasthttp/router v1.4.20/go.mod h1:um867yNQKtERxBm+C+yzgWxjspTiQoA8z86Ec3fK/tc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -196,10 +193,11 @@ github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmnc github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= @@ -209,8 +207,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= @@ -221,6 +219,7 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= @@ -238,6 +237,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -308,12 +309,12 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -359,10 +360,11 @@ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -383,7 +385,6 @@ github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= 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/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -447,8 +448,8 @@ github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY= github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -473,9 +474,9 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 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= @@ -500,14 +501,16 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU= +github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -535,39 +538,43 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36tZeQ= github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d h1:Q+gqLBOPkFGHyCJxXMRqtUgUbTjI8/Ze8vu8GGyNFwo= -github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -576,21 +583,19 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= @@ -610,12 +615,11 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= @@ -645,8 +649,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= @@ -691,7 +695,6 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -732,6 +735,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -749,7 +754,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -776,7 +780,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= @@ -794,6 +798,7 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -877,9 +882,10 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -915,7 +921,6 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -957,6 +962,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 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= @@ -1107,9 +1114,9 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/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= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= helm.sh/helm/v3 v3.10.0 h1:y/MYONZ/bsld9kHwqgBX2uPggnUr5hahpjwt9/jrHlI= helm.sh/helm/v3 v3.10.0/go.mod h1:paPw0hO5KVfrCMbi1M8+P8xdfBri3IiJiVKATZsFR94= @@ -1120,12 +1127,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0= -k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk= +k8s.io/api v0.25.2 h1:v6G8RyFcwf0HR5jQGIAYlvtRNrxMJQG1xJzaSeVnIS8= +k8s.io/api v0.25.2/go.mod h1:qP1Rn4sCVFwx/xIhe+we2cwBLTXNcheRyYXwajonhy0= k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= -k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= -k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= +k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs= +k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA= k8s.io/apiserver v0.25.0 h1:8kl2ifbNffD440MyvHtPaIz1mw4mGKVgWqM0nL+oyu4= k8s.io/apiserver v0.25.0/go.mod h1:BKwsE+PTC+aZK+6OJQDPr0v6uS91/HWxX7evElAH6xo= k8s.io/cli-runtime v0.25.0 h1:XBnTc2Fi+w818jcJGzhiJKQuXl8479sZ4FhtV5hVJ1Q= @@ -1134,15 +1141,14 @@ k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E= k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8= k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= -k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= k8s.io/kubectl v0.25.0 h1:/Wn1cFqo8ik3iee1EvpxYre3bkWsGLXzLQI6uCCAkQc= k8s.io/kubectl v0.25.0/go.mod h1:n16ULWsOl2jmQpzt2o7Dud1t4o0+Y186ICb4O+GwKAU= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4= oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/api/magefile.go b/api/magefile.go index 33aa0211d..a4ee43585 100644 --- a/api/magefile.go +++ b/api/magefile.go @@ -36,7 +36,7 @@ func TestWithCoa() error { func testHelper() error { if err := shellcmd.RunAll( "go clean -testcache", - "go test -race -timeout 35s -cover ./...", + "go test -race -timeout 5m -cover -coverprofile=coverage.out ./...", ); err != nil { return err } diff --git a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go index 1a7883f67..c043e27e5 100644 --- a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go @@ -1,3 +1,9 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + package catalogs import ( diff --git a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go index 0bccc2576..1b5cecbc0 100644 --- a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go +++ b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go @@ -10,8 +10,10 @@ import ( "context" "encoding/json" "fmt" + "os" "time" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" @@ -29,14 +31,16 @@ var log = logger.NewLogger("coa.runtime") type JobsManager struct { managers.Manager StateProvider states.IStateProvider + apiClient utils.ApiClient + interval int32 } type LastSuccessTime struct { Time time.Time `json:"time"` } -func (s *JobsManager) Init(context *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error { - err := s.Manager.Init(context, config, providers) +func (s *JobsManager) Init(vContext *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error { + err := s.Manager.Init(vContext, config, providers) if err != nil { return err } @@ -47,6 +51,41 @@ func (s *JobsManager) Init(context *contexts.VendorContext, config managers.Mana } else { return err } + + baseUrl, err := utils.GetString(s.Manager.Config.Properties, "baseUrl") + if err != nil { + return err + } + + s.interval = utils.ReadInt32(s.Manager.Config.Properties, "interval", 0) + + clientOptions := make([]utils.ApiClientOption, 0) + + if caCert, ok := os.LookupEnv(constants.ApiCertEnvName); ok { + clientOptions = append(clientOptions, utils.WithCertAuth(caCert)) + } + + if utils.ShouldUseSATokens() { + clientOptions = append(clientOptions, utils.WithServiceAccountToken()) + } else { + user, err := utils.GetString(s.Manager.Config.Properties, "user") + if err != nil { + return err + } + + password, err := utils.GetString(s.Manager.Config.Properties, "password") + if err != nil { + return err + } + clientOptions = append(clientOptions, utils.WithUserPassword(context.TODO(), user, password)) + } + + client, err := utils.NewAPIClient(context.Background(), baseUrl, clientOptions...) + if err != nil { + return err + } + + s.apiClient = client return nil } @@ -61,26 +100,12 @@ func (s *JobsManager) pollObjects() []error { var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - var baseUrl, user, password string - baseUrl, err = utils.GetString(s.Manager.Config.Properties, "baseUrl") - if err != nil { - return []error{err} - } - user, err = utils.GetString(s.Manager.Config.Properties, "user") - if err != nil { - return []error{err} - } - password, err = utils.GetString(s.Manager.Config.Properties, "password") - if err != nil { - return []error{err} - } - interval := utils.ReadInt32(s.Manager.Config.Properties, "interval", 0) - if interval == 0 { + if s.interval == 0 { return nil } var instances []model.InstanceState - instances, err = utils.GetInstancesForAllNamespaces(context, baseUrl, user, password) + instances, err = s.apiClient.GetInstancesForAllNamespaces(context) if err != nil { fmt.Println(err.Error()) return []error{err} @@ -89,12 +114,15 @@ func (s *JobsManager) pollObjects() []error { var entry states.StateEntry entry, err = s.StateProvider.Get(context, states.GetRequest{ ID: "i_" + instance.ObjectMeta.Name, + Metadata: map[string]interface{}{ + "namespace": instance.ObjectMeta.Namespace, + }, }) needsPub := true if err == nil { var stamp LastSuccessTime if stamp, err = getLastSuccessTime(entry.Body); err == nil { - if time.Since(stamp.Time) > time.Duration(interval)*time.Second { //TODO: compare object hash as well? + if time.Since(stamp.Time) > time.Duration(s.interval)*time.Second { //TODO: compare object hash as well? needsPub = true } else { needsPub = false @@ -109,12 +137,13 @@ func (s *JobsManager) pollObjects() []error { Body: v1alpha2.JobData{ Id: instance.ObjectMeta.Name, Action: v1alpha2.JobUpdate, + Scope: instance.ObjectMeta.Namespace, }, }) } } var targets []model.TargetState - targets, err = utils.GetTargetsForAllNamespaces(context, baseUrl, user, password) + targets, err = s.apiClient.GetTargetsForAllNamespaces(context) if err != nil { fmt.Println(err.Error()) return []error{err} @@ -123,12 +152,15 @@ func (s *JobsManager) pollObjects() []error { var entry states.StateEntry entry, err = s.StateProvider.Get(context, states.GetRequest{ ID: "t_" + target.ObjectMeta.Name, + Metadata: map[string]interface{}{ + "namespace": target.ObjectMeta.Namespace, + }, }) needsPub := true if err == nil { var stamp LastSuccessTime if stamp, err = getLastSuccessTime(entry.Body); err == nil { - if time.Since(stamp.Time) > time.Duration(interval)*time.Second { //TODO: compare object hash as well? + if time.Since(stamp.Time) > time.Duration(s.interval)*time.Second { //TODO: compare object hash as well? needsPub = true } else { needsPub = false @@ -143,6 +175,7 @@ func (s *JobsManager) pollObjects() []error { Body: v1alpha2.JobData{ Id: target.ObjectMeta.Name, Action: v1alpha2.JobUpdate, + Scope: target.ObjectMeta.Namespace, }, }) } @@ -234,6 +267,7 @@ func (s *JobsManager) HandleHeartBeatEvent(ctx context.Context, event v1alpha2.E namespace = "default" } // TODO: the heart beat data should contain a "finished" field so data can be cleared + log.Debugf(" M (Job): handling heartbeat h_%s", heartbeat.JobId) _, err = s.StateProvider.Upsert(ctx, states.UpsertRequest{ Value: states.StateEntry{ ID: "h_" + heartbeat.JobId, @@ -267,8 +301,10 @@ func (s *JobsManager) DelayOrSkipJob(ctx context.Context, namespace string, obje }) if err != nil { if !v1alpha2.IsNotFound(err) { + log.Errorf(" M (Job): error getting heartbeat %s: %s", key, err.Error()) return err } + log.Debugf(" M (Job): found heartbeat %s, entry: %+v", key, entry) return nil // no heartbeat } var heartbeat v1alpha2.HeartBeatData @@ -328,9 +364,6 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) if objectType, ok := event.Metadata["objectType"]; ok { var job v1alpha2.JobData - var baseUrl string - var user string - var password string jData, _ := json.Marshal(event.Body) err = json.Unmarshal(jData, &job) if err != nil { @@ -342,33 +375,21 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) return err } - baseUrl, err = utils.GetString(s.Manager.Config.Properties, "baseUrl") - if err != nil { - return err - } - user, err = utils.GetString(s.Manager.Config.Properties, "user") - if err != nil { - return err - } - password, err = utils.GetString(s.Manager.Config.Properties, "password") - if err != nil { - return err - } switch objectType { case "instance": log.Debugf(" M (Job): handling instance job %s", job.Id) instanceName := job.Id var instance model.InstanceState //get intance - instance, err = utils.GetInstance(ctx, baseUrl, instanceName, user, password, namespace) + instance, err = s.apiClient.GetInstance(ctx, instanceName, namespace) if err != nil { - log.Errorf(" M (Job): error getting instance %s: %s", instanceName, err.Error()) + log.Errorf(" M (Job): error getting instance %s, namespace: %s: %s", instanceName, namespace, err.Error()) return err //TODO: instance is gone } //get solution var solution model.SolutionState - solution, err = utils.GetSolution(ctx, baseUrl, instance.Spec.Solution, user, password, namespace) + solution, err = s.apiClient.GetSolution(ctx, instance.Spec.Solution, namespace) if err != nil { solution = model.SolutionState{ ObjectMeta: model.ObjectMeta{ @@ -383,7 +404,7 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) //get targets var targets []model.TargetState - targets, err = utils.GetTargets(ctx, baseUrl, user, password, namespace) + targets, err = s.apiClient.GetTargets(ctx, namespace) if err != nil { targets = make([]model.TargetState, 0) } @@ -393,7 +414,7 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) //create deployment spec var deployment model.DeploymentSpec - deployment, err = utils.CreateSymphonyDeployment(instance, solution, targetCandidates, nil) + deployment, err = utils.CreateSymphonyDeployment(instance, solution, targetCandidates, nil, namespace) if err != nil { log.Errorf(" M (Job): error creating deployment spec for instance %s: %s", instanceName, err.Error()) return err @@ -402,7 +423,7 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) //call api switch job.Action { case v1alpha2.JobUpdate: - _, err = utils.Reconcile(ctx, baseUrl, user, password, deployment, namespace, false) + _, err = s.apiClient.Reconcile(ctx, deployment, false, namespace) if err != nil { log.Errorf(" M (Job): error reconciling instance %s: %s", instanceName, err.Error()) return err @@ -420,11 +441,11 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) }) } case v1alpha2.JobDelete: - _, err = utils.Reconcile(ctx, baseUrl, user, password, deployment, namespace, true) + _, err = s.apiClient.Reconcile(ctx, deployment, true, namespace) if err != nil { return err } else { - return utils.DeleteInstance(ctx, baseUrl, deployment.Instance.Spec.Name, user, password, namespace) + return s.apiClient.DeleteInstance(ctx, deployment.Instance.Spec.Name, namespace) } default: return v1alpha2.NewCOAError(nil, "unsupported action", v1alpha2.BadRequest) @@ -432,18 +453,18 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) case "target": var target model.TargetState targetName := job.Id - target, err = utils.GetTarget(ctx, baseUrl, targetName, user, password, namespace) + target, err = s.apiClient.GetTarget(ctx, targetName, namespace) if err != nil { return err } var deployment model.DeploymentSpec - deployment, err = utils.CreateSymphonyDeploymentFromTarget(target) + deployment, err = utils.CreateSymphonyDeploymentFromTarget(target, namespace) if err != nil { return err } switch job.Action { case v1alpha2.JobUpdate: - _, err = utils.Reconcile(ctx, baseUrl, user, password, deployment, namespace, false) + _, err = s.apiClient.Reconcile(ctx, deployment, false, namespace) if err != nil { return err } else { @@ -461,15 +482,49 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) }) } case v1alpha2.JobDelete: - _, err = utils.Reconcile(ctx, baseUrl, user, password, deployment, namespace, true) + _, err = s.apiClient.Reconcile(ctx, deployment, true, namespace) if err != nil { return err } else { - return utils.DeleteTarget(ctx, baseUrl, targetName, user, password, namespace) + return s.apiClient.DeleteTarget(ctx, targetName, namespace) } default: return v1alpha2.NewCOAError(nil, "unsupported action", v1alpha2.BadRequest) } + case "deployment": + log.Infof(" M (Job): handling deployment job %s, action: %s", job.Id, job.Action) + log.Infof(" M (Job): deployment spec: %s", string(job.Data)) + + var deployment *model.DeploymentSpec + deployment, err = model.ToDeployment(job.Data) + if err != nil { + return err + } + if job.Action == v1alpha2.JobUpdate { + _, err = s.apiClient.Reconcile(ctx, *deployment, false, namespace) + if err != nil { + return err + } else { + // TODO: how to handle status updates? + s.StateProvider.Upsert(ctx, states.UpsertRequest{ + Value: states.StateEntry{ + ID: "d_" + deployment.Instance.Spec.Name, + Body: LastSuccessTime{ + Time: time.Now().UTC(), + }, + }, + Metadata: map[string]interface{}{ + "namespace": namespace, + }, + }) + } + } + if job.Action == v1alpha2.JobDelete { + _, err = s.apiClient.Reconcile(ctx, *deployment, true, namespace) + if err != nil { + return err + } + } } } return nil diff --git a/api/pkg/apis/v1alpha1/managers/solution/metrics/attributes.go b/api/pkg/apis/v1alpha1/managers/solution/metrics/attributes.go new file mode 100644 index 000000000..a6d5c2013 --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/solution/metrics/attributes.go @@ -0,0 +1,18 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package metrics + +// Deployment gets common logging attributes for a deployment. +func Deployment( + opeartion string, + operationType string, +) map[string]any { + return map[string]any{ + "operation": opeartion, + "operationType": operationType, + } +} diff --git a/api/pkg/apis/v1alpha1/managers/solution/metrics/metrics.go b/api/pkg/apis/v1alpha1/managers/solution/metrics/metrics.go new file mode 100644 index 000000000..976922b2e --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/solution/metrics/metrics.go @@ -0,0 +1,62 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package metrics + +import ( + "github.com/eclipse-symphony/symphony/api/constants" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" +) + +const ( + GetSummaryOperation string = "GetSummary" + + GetOperationType string = "Get" +) + +// Metrics is a metrics tracker for an api operation. +type Metrics struct { + apiComponentCount observability.Gauge +} + +func New() (*Metrics, error) { + observable := observability.New(constants.API) + + apiComponentCount, err := observable.Metrics.Gauge( + "symphony_api_component_count", + "count of components in API operation", + ) + if err != nil { + return nil, err + } + + return &Metrics{ + apiComponentCount: apiComponentCount, + }, nil +} + +// Close closes all metrics. +func (m *Metrics) Close() { +} + +// ApiComponentCount gets the total count of components for an API operation. +func (m *Metrics) ApiComponentCount( + componentCount int, + operation string, + operationType string, +) { + if m == nil { + return + } + + m.apiComponentCount.Set( + float64(componentCount), + Deployment( + operation, + operationType, + ), + ) +} diff --git a/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go b/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go index 903efc51f..d782ad0bb 100644 --- a/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go +++ b/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go @@ -17,6 +17,7 @@ import ( "sync" "time" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/solution/metrics" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" sp "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers" tgt "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target" @@ -33,12 +34,22 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/logger" ) -var log = logger.NewLogger("coa.runtime") -var lock sync.Mutex +var ( + log = logger.NewLogger("coa.runtime") + lock sync.Mutex + apiOperationMetrics *metrics.Metrics +) const ( SYMPHONY_AGENT string = "/symphony-agent:" ENV_NAME string = "SYMPHONY_AGENT_ADDRESS" + + // DeploymentType_Update indicates the type of deployment is Update. This is + // to give a deployment status on Symphony Target deployment. + DeploymentType_Update string = "Target Update" + // DeploymentType_Delete indicates the type of deployment is Delete. This is + // to give a deployment status on Symphony Target deployment. + DeploymentType_Delete string = "Target Delete" ) type SolutionManager struct { @@ -46,7 +57,7 @@ type SolutionManager struct { TargetProviders map[string]tgt.ITargetProvider StateProvider states.IStateProvider ConfigProvider config.IExtConfigProvider - SecretProvoider secret.ISecretProvider + SecretProvider secret.ISecretProvider IsTarget bool TargetNames []string } @@ -84,7 +95,7 @@ func (s *SolutionManager) Init(context *contexts.VendorContext, config managers. secretProvider, err := managers.GetSecretProvider(config, providers) if err == nil { - s.SecretProvoider = secretProvider + s.SecretProvider = secretProvider } else { return err } @@ -114,6 +125,13 @@ func (s *SolutionManager) Init(context *contexts.VendorContext, config managers. } } + if apiOperationMetrics == nil { + apiOperationMetrics, err = metrics.New() + if err != nil { + return err + } + } + return nil } @@ -145,7 +163,7 @@ func (s *SolutionManager) GetSummary(ctx context.Context, key string, namespace var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - log.Info(" M (Solution): get summary") + log.Infof(" M (Solution): get summary, key: %s, namespace: %s, traceId: %s", key, namespace, span.SpanContext().TraceID().String()) var state states.StateEntry state, err = s.StateProvider.Get(iCtx, states.GetRequest{ @@ -185,6 +203,7 @@ func (s *SolutionManager) sendHeartbeat(id string, namespace string, remove bool s.VendorContext.Publish("heartbeat", v1alpha2.Event{ Body: v1alpha2.HeartBeatData{ JobId: id, + Scope: namespace, Action: action, Time: time.Now().UTC(), }, @@ -212,7 +231,13 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - log.Info(" M (Solution): reconciling") + log.Infof(" M (Solution): reconciling deployment.InstanceName: %s, deployment.SolutionName: %s, remove: %t, namespace: %s, targetName: %s, traceId: %s", + deployment.Instance.Spec.Name, + deployment.SolutionName, + remove, + namespace, + targetName, + span.SpanContext().TraceID().String()) summary := model.SummarySpec{ TargetResults: make(map[string]model.TargetResultSpec), @@ -221,6 +246,25 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy AllAssignedDeployed: false, } + deploymentType := DeploymentType_Update + if remove { + deploymentType = DeploymentType_Delete + } + summary.IsRemoval = remove + + s.saveSummaryProgress(ctx, deployment, summary, namespace) + defer func() { + s.concludeSummary(ctx, deployment, summary, namespace) + }() + + // get the components count for the deployment + componentCount := len(deployment.Solution.Spec.Components) + apiOperationMetrics.ApiComponentCount( + componentCount, + metrics.GetSummaryOperation, + metrics.GetOperationType, + ) + if s.VendorContext != nil && s.VendorContext.EvaluationContext != nil { context := s.VendorContext.EvaluationContext.Clone() context.DeploymentSpec = deployment @@ -236,7 +280,6 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy } else { summary.SummaryMessage = "failed to evaluate deployment spec: " + err.Error() log.Errorf(" M (Solution): failed to evaluate deployment spec: %+v", err) - s.saveSummary(iCtx, deployment, summary, namespace) return summary, err } } @@ -248,14 +291,12 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy if err != nil { summary.SummaryMessage = "failed to create target manager state from deployment spec: " + err.Error() log.Errorf(" M (Solution): failed to create target manager state from deployment spec: %+v", err) - s.saveSummary(iCtx, deployment, summary, namespace) return summary, err } currentState, _, err = s.Get(iCtx, deployment, targetName) if err != nil { summary.SummaryMessage = "failed to get current state: " + err.Error() log.Errorf(" M (Solution): failed to get current state: %+v", err) - s.saveSummary(iCtx, deployment, summary, namespace) return summary, err } desiredState := currentDesiredState @@ -268,16 +309,20 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy } mergedState := MergeDeploymentStates(¤tState, desiredState) - var plan model.DeploymentPlan plan, err = PlanForDeployment(deployment, mergedState) if err != nil { summary.SummaryMessage = "failed to plan for deployment: " + err.Error() log.Errorf(" M (Solution): failed to plan for deployment: %+v", err) - s.saveSummary(iCtx, deployment, summary, namespace) return summary, err } + planBytes, _ := json.Marshal(plan) + log.Debugf(" M (Solution): deployment plan: %s", string(planBytes)) + + mergedStateBytes, _ := json.Marshal(mergedState) + log.Debugf(" M (Solution): merged state: %s", string(mergedStateBytes)) + col := api_utils.MergeCollection(deployment.Solution.Spec.Metadata, deployment.Instance.Spec.Metadata) dep := deployment dep.Instance.Spec.Metadata = col @@ -288,6 +333,7 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy plannedCount := 0 planSuccessCount := 0 for _, step := range plan.Steps { + log.Debugf(" M (Solution): processing step: %+v", step) if s.IsTarget && !api_utils.ContainsString(s.TargetNames, step.Target) { continue } @@ -320,7 +366,6 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy if err != nil { summary.SummaryMessage = "failed to create provider:" + err.Error() log.Errorf(" M (Solution): failed to create provider: %+v", err) - s.saveSummary(ctx, deployment, summary, namespace) return summary, err } } else { @@ -335,6 +380,7 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy continue } } + log.Debugf(" M (Solution): applying step: %+v", step) someStepsRan = true retryCount := 1 //TODO: set to 1 for now. Although retrying can help to handle transient errors, in more cases @@ -369,12 +415,15 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy targetResult[step.Target] = 1 summary.AllAssignedDeployed = plannedCount == planSuccessCount summary.UpdateTargetResult(step.Target, model.TargetResultSpec{Status: "OK", Message: "", ComponentResults: componentResults}) + s.saveSummaryProgress(ctx, deployment, summary, namespace) break } else { targetResult[step.Target] = 0 summary.AllAssignedDeployed = false - summary.UpdateTargetResult(step.Target, model.TargetResultSpec{Status: "Error", Message: stepError.Error(), ComponentResults: componentResults}) // TODO: this keeps only the last error on the target - time.Sleep(5 * time.Second) //TODO: make this configurable? + targetResultStatus := fmt.Sprintf("%s Failed", deploymentType) + targetResultMessage := fmt.Sprintf("An error occurred in %s, err: %s", deploymentType, stepError.Error()) + summary.UpdateTargetResult(step.Target, model.TargetResultSpec{Status: targetResultStatus, Message: targetResultMessage, ComponentResults: componentResults}) // TODO: this keeps only the last error on the target + time.Sleep(5 * time.Second) //TODO: make this configurable? } } if stepError != nil { @@ -386,7 +435,6 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy } summary.SuccessCount = successCount summary.AllAssignedDeployed = plannedCount == planSuccessCount - s.saveSummary(iCtx, deployment, summary, namespace) err = stepError return summary, err } @@ -436,12 +484,10 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy summary.SuccessCount = summary.TargetCount } - s.saveSummary(iCtx, deployment, summary, namespace) - return summary, nil } -// The dployment spec may have changed, so the previous target is not in the new deployment anymore +// The deployment spec may have changed, so the previous target is not in the new deployment anymore func (s *SolutionManager) getTargetStateForStep(step model.DeploymentStep, deployment model.DeploymentSpec, previousDeploymentState *SolutionManagerDeploymentState) model.TargetState { //first find the target spec in the deployment targetSpec, ok := deployment.Targets[step.Target] @@ -453,15 +499,17 @@ func (s *SolutionManager) getTargetStateForStep(step model.DeploymentStep, deplo return targetSpec } -func (s *SolutionManager) saveSummary(ctx context.Context, deployment model.DeploymentSpec, summary model.SummarySpec, namespace string) { +func (s *SolutionManager) saveSummary(ctx context.Context, deployment model.DeploymentSpec, summary model.SummarySpec, state model.SummaryState, namespace string) { // TODO: delete this state when time expires. This should probably be invoked by the vendor (via GetSummary method, for instance) s.StateProvider.Upsert(ctx, states.UpsertRequest{ Value: states.StateEntry{ ID: fmt.Sprintf("%s-%s", "summary", deployment.Instance.Spec.Name), Body: model.SummaryResult{ - Summary: summary, - Generation: deployment.Generation, - Time: time.Now().UTC(), + Summary: summary, + Generation: deployment.Generation, + Time: time.Now().UTC(), + State: state, + DeploymentHash: deployment.Hash, }, }, Metadata: map[string]interface{}{ @@ -469,6 +517,15 @@ func (s *SolutionManager) saveSummary(ctx context.Context, deployment model.Depl }, }) } + +func (s *SolutionManager) saveSummaryProgress(ctx context.Context, deployment model.DeploymentSpec, summary model.SummarySpec, namespace string) { + s.saveSummary(ctx, deployment, summary, model.SummaryStateRunning, namespace) +} + +func (s *SolutionManager) concludeSummary(ctx context.Context, deployment model.DeploymentSpec, summary model.SummarySpec, namespace string) { + s.saveSummary(ctx, deployment, summary, model.SummaryStateDone, namespace) +} + func (s *SolutionManager) canSkipStep(ctx context.Context, step model.DeploymentStep, target string, provider tgt.ITargetProvider, currentComponents []model.ComponentSpec, state model.DeploymentState) bool { for _, newCom := range step.Components { @@ -505,7 +562,11 @@ func (s *SolutionManager) Get(ctx context.Context, deployment model.DeploymentSp }) var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - log.Info(" M (Solution): getting deployment") + log.Infof(" M (Solution): getting deployment.InstanceName: %s, deployment.SolutionName: %s, targetName: %s, traceId: %s", + deployment.Instance.Spec.Name, + deployment.SolutionName, + targetName, + span.SpanContext().TraceID().String()) ret := model.DeploymentState{} @@ -558,7 +619,7 @@ func (s *SolutionManager) Get(ctx context.Context, deployment model.DeploymentSp components, err = (provider.(tgt.ITargetProvider)).Get(iCtx, deployment, step.Components) if err != nil { - log.Errorf(" M (Solution): failed to get: %+v", err) + log.Warnf(" M (Solution): failed to get components: %+v", err) return ret, nil, err } for _, c := range components { diff --git a/api/pkg/apis/v1alpha1/model/deployableStatus.go b/api/pkg/apis/v1alpha1/model/deployableStatus.go new file mode 100644 index 000000000..c6415a09b --- /dev/null +++ b/api/pkg/apis/v1alpha1/model/deployableStatus.go @@ -0,0 +1,15 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package model + +import "time" + +type DeployableStatus struct { + Properties map[string]string `json:"properties,omitempty"` + ProvisioningStatus ProvisioningStatus `json:"provisioningStatus"` + LastModified time.Time `json:"lastModified,omitempty"` +} diff --git a/api/pkg/apis/v1alpha1/model/deployment.go b/api/pkg/apis/v1alpha1/model/deployment.go index 1c7d06240..a8d880d6e 100644 --- a/api/pkg/apis/v1alpha1/model/deployment.go +++ b/api/pkg/apis/v1alpha1/model/deployment.go @@ -23,6 +23,8 @@ type DeploymentSpec struct { ComponentEndIndex int `json:"componentEndIndex,omitempty"` ActiveTarget string `json:"activeTarget,omitempty"` Generation string `json:"generation,omitempty"` + ObjectNamespace string `json:"objectNamespace,omitempty"` + Hash string `json:"hash,omitempty"` } func (d DeploymentSpec) GetComponentSlice() []ComponentSpec { diff --git a/api/pkg/apis/v1alpha1/model/instance.go b/api/pkg/apis/v1alpha1/model/instance.go index 38ec20d50..4b6389882 100644 --- a/api/pkg/apis/v1alpha1/model/instance.go +++ b/api/pkg/apis/v1alpha1/model/instance.go @@ -8,16 +8,10 @@ package model import ( "errors" - "time" ) type ( - InstanceStatus struct { - // Important: Run "make" to regenerate code after modifying this file - Properties map[string]string `json:"properties,omitempty"` - ProvisioningStatus ProvisioningStatus `json:"provisioningStatus"` - LastModified time.Time `json:"lastModified,omitempty"` - } + InstanceStatus = DeployableStatus // InstanceState defines the current state of the instance InstanceState struct { diff --git a/api/pkg/apis/v1alpha1/model/plan.go b/api/pkg/apis/v1alpha1/model/plan.go index 5cbcc1472..fbbaacefb 100644 --- a/api/pkg/apis/v1alpha1/model/plan.go +++ b/api/pkg/apis/v1alpha1/model/plan.go @@ -7,6 +7,7 @@ package model import ( + "fmt" "strings" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" @@ -55,7 +56,7 @@ func (s DeploymentStep) PrepareResultMap() map[string]ComponentResultSpec { for _, c := range s.Components { ret[c.Component.Name] = ComponentResultSpec{ Status: v1alpha2.Untouched, - Message: "", + Message: fmt.Sprintf("No error. %s is untouched", c.Component.Name), } } return ret diff --git a/api/pkg/apis/v1alpha1/model/plan_test.go b/api/pkg/apis/v1alpha1/model/plan_test.go index 9c9b44066..95a9ed5a2 100644 --- a/api/pkg/apis/v1alpha1/model/plan_test.go +++ b/api/pkg/apis/v1alpha1/model/plan_test.go @@ -7,6 +7,7 @@ package model import ( + "fmt" "testing" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" @@ -218,8 +219,8 @@ func createSampleDeploymentStepWithDeleteComponent() DeploymentStep { func TestPrepareResultMap(t *testing.T) { s := createSampleDeploymentStepWithUpdateComponent() resultMap := s.PrepareResultMap() - assert.Equal(t, resultMap["sample-grpc-solution"].Status, v1alpha2.Untouched) - assert.Equal(t, resultMap["sample-grpc-solution"].Message, "") + assert.Equal(t, v1alpha2.Untouched, resultMap["sample-grpc-solution"].Status) + assert.Equal(t, fmt.Sprintf("No error. %s is untouched", "sample-grpc-solution"), resultMap["sample-grpc-solution"].Message) } func TestGetComponents(t *testing.T) { diff --git a/api/pkg/apis/v1alpha1/model/summary.go b/api/pkg/apis/v1alpha1/model/summary.go index b830bbed9..f63b068d5 100644 --- a/api/pkg/apis/v1alpha1/model/summary.go +++ b/api/pkg/apis/v1alpha1/model/summary.go @@ -33,11 +33,21 @@ type SummarySpec struct { AllAssignedDeployed bool `json:"allAssignedDeployed"` } type SummaryResult struct { - Summary SummarySpec `json:"summary"` - Generation string `json:"generation"` - Time time.Time `json:"time"` + Summary SummarySpec `json:"summary"` + Generation string `json:"generation"` + Time time.Time `json:"time"` + State SummaryState `json:"state"` + DeploymentHash string `json:"deploymentHash"` } +const ( + SummaryStatePending SummaryState = iota // Currently unused + SummaryStateRunning // Should indicate that a reconcile operation is in progress + SummaryStateDone // Should indicate that a reconcile operation has completed either successfully or unsuccessfully +) + +type SummaryState int + func (s *SummarySpec) UpdateTargetResult(target string, spec TargetResultSpec) { if v, ok := s.TargetResults[target]; !ok { s.TargetResults[target] = spec @@ -59,3 +69,7 @@ func (s *SummarySpec) UpdateTargetResult(target string, spec TargetResultSpec) { s.TargetResults[target] = v } } + +func (summary *SummaryResult) IsDeploymentFinished() bool { + return summary.State == SummaryStateDone +} diff --git a/api/pkg/apis/v1alpha1/model/summary_test.go b/api/pkg/apis/v1alpha1/model/summary_test.go index daceced92..479ee935a 100644 --- a/api/pkg/apis/v1alpha1/model/summary_test.go +++ b/api/pkg/apis/v1alpha1/model/summary_test.go @@ -9,6 +9,7 @@ package model import ( "testing" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/stretchr/testify/assert" ) @@ -32,3 +33,32 @@ func TestUpdateTargetResult(t *testing.T) { assert.Equal(t, "ERROR", s.TargetResults["target2"].Status) assert.Equal(t, 0, s.SuccessCount) //ver 0.48.1: UpdateTargetResult no longer updates success count } + +func TestUpdateTargetResultWithComponentResults(t *testing.T) { + s := &SummarySpec{ + TargetResults: map[string]TargetResultSpec{ + "target1": { + Status: "OK", + ComponentResults: map[string]ComponentResultSpec{ + "component1": { + Status: v1alpha2.Accepted, + Message: "Component 1 is accepted", + }, + }, + }, + }, + } + s.UpdateTargetResult("target1", TargetResultSpec{ + Status: "ERROR", + ComponentResults: map[string]ComponentResultSpec{ + "component1": { + Status: v1alpha2.BadConfig, + Message: "Component 1 is in bad config", + }, + }, + }) + assert.Equal(t, "ERROR", s.TargetResults["target1"].Status) + assert.Equal(t, v1alpha2.BadConfig, s.TargetResults["target1"].ComponentResults["component1"].Status) + assert.Equal(t, "Component 1 is in bad config", s.TargetResults["target1"].ComponentResults["component1"].Message) + assert.Equal(t, 0, s.SuccessCount) //ver 0.48.1: UpdateTargetResult no longer updates success count +} diff --git a/api/pkg/apis/v1alpha1/model/target.go b/api/pkg/apis/v1alpha1/model/target.go index fcd2211bd..919880336 100644 --- a/api/pkg/apis/v1alpha1/model/target.go +++ b/api/pkg/apis/v1alpha1/model/target.go @@ -8,15 +8,11 @@ package model import ( "errors" - "time" ) type ( - TargetStatus struct { - Properties map[string]string `json:"properties,omitempty"` - ProvisioningStatus ProvisioningStatus `json:"provisioningStatus"` - LastModified time.Time `json:"lastModified,omitempty"` - } + TargetStatus = DeployableStatus + // TargetState defines the current state of the target TargetState struct { ObjectMeta ObjectMeta `json:"metadata,omitempty"` diff --git a/api/pkg/apis/v1alpha1/model/utils.go b/api/pkg/apis/v1alpha1/model/utils.go index 8674597e6..ca5fcd1ea 100644 --- a/api/pkg/apis/v1alpha1/model/utils.go +++ b/api/pkg/apis/v1alpha1/model/utils.go @@ -7,6 +7,7 @@ package model import ( + "encoding/json" "fmt" "strings" @@ -345,3 +346,12 @@ func ResolveString(value string, injections *ValueInjections) string { return value } + +func ToDeployment(data []byte) (*DeploymentSpec, error) { + var deployment DeploymentSpec + err := json.Unmarshal(data, &deployment) + if err != nil { + return nil, err + } + return &deployment, nil +} diff --git a/api/pkg/apis/v1alpha1/model/validationrule.go b/api/pkg/apis/v1alpha1/model/validationrule.go index 8ce8da593..356dd7b8f 100644 --- a/api/pkg/apis/v1alpha1/model/validationrule.go +++ b/api/pkg/apis/v1alpha1/model/validationrule.go @@ -20,6 +20,8 @@ type PropertyDesc struct { SkipIfMissing bool `json:"skipIfMissing,omitempty"` PrefixMatch bool `json:"prefixMatch,omitempty"` IsComponentName bool `json:"isComponentName,omitempty"` + // This is a stop-gap solution to support change detection for advanced comparison scenarios. + PropChanged func(oldProp, newProp any) bool `json:"-"` } type ComponentValidationRule struct { RequiredComponentType string `json:"requiredType"` @@ -157,8 +159,13 @@ func compareStrings(a, b string, ignoreCase bool, prefixMatch bool) bool { } } func compareProperties(c PropertyDesc, old map[string]interface{}, new map[string]interface{}, key string) bool { - if v, ok := old[key]; ok { - if nv, nok := new[key]; nok { + v, ook := old[key] + nv, nok := new[key] + if c.PropChanged != nil { + return c.PropChanged(v, nv) + } + if ook { + if nok { if !compareStrings(fmt.Sprintf("%v", v), fmt.Sprintf("%v", nv), c.IgnoreCase, c.PrefixMatch) { return true } diff --git a/api/pkg/apis/v1alpha1/model/validationrule_test.go b/api/pkg/apis/v1alpha1/model/validationrule_test.go index fd6cea535..5c53991e9 100644 --- a/api/pkg/apis/v1alpha1/model/validationrule_test.go +++ b/api/pkg/apis/v1alpha1/model/validationrule_test.go @@ -667,3 +667,93 @@ func TestCheckSidecarEnvVarChange(t *testing.T) { equal := validationRule.IsComponentChanged(components1, components2) assert.True(t, equal) } + +func TestValidateChangeDetectionNoChange(t *testing.T) { + rule := ValidationRule{ + ComponentValidationRule: ComponentValidationRule{ + ChangeDetectionProperties: []PropertyDesc{ + {Name: "prop", IgnoreCase: false, SkipIfMissing: true}, + }, + }, + } + oldComponent := ComponentSpec{ + Properties: map[string]interface{}{ + "prop": map[string]interface{}{ + "propa": "valuea", + "propb": "valueb", + }, + }, + Name: "comp", + } + newComponent := ComponentSpec{ + Properties: map[string]interface{}{ + "prop": map[string]interface{}{ + "propa": "valuea", + "propb": "valueb", + }, + }, + Name: "comp", + } + assert.False(t, rule.IsComponentChanged(oldComponent, newComponent)) +} + +func TestValidateChangeDetectionWithChange(t *testing.T) { + rule := ValidationRule{ + ComponentValidationRule: ComponentValidationRule{ + ChangeDetectionProperties: []PropertyDesc{ + {Name: "prop", IgnoreCase: false, SkipIfMissing: true}, + }, + }, + } + oldComponent := ComponentSpec{ + Properties: map[string]interface{}{ + "prop": map[string]interface{}{ + "propa": "valuea", + "propb": "valueb", + }, + }, + Name: "comp", + } + newComponent := ComponentSpec{ + Properties: map[string]interface{}{ + "prop": map[string]interface{}{ + "propa": "valuea", + "propb": "changed valueb", + }, + }, + Name: "comp", + } + assert.True(t, rule.IsComponentChanged(oldComponent, newComponent)) +} + +func TestValidateChangeDetectionWithCustomComparator(t *testing.T) { + rule := ValidationRule{ + ComponentValidationRule: ComponentValidationRule{ + ChangeDetectionProperties: []PropertyDesc{ + {Name: "prop", PropChanged: func(_, _ any) bool { + // always return true + return true + }}, + }, + }, + } + oldComponent := ComponentSpec{ + Properties: map[string]interface{}{ + "prop": map[string]interface{}{ + "propa": "valuea", + "propb": "valueb", + }, + }, + Name: "comp", + } + newComponent := ComponentSpec{ + Properties: map[string]interface{}{ + "prop": map[string]interface{}{ + "propa": "valuea", + "propb": "valueb", + }, + }, + Name: "comp", + } + assert.True(t, rule.IsComponentChanged(oldComponent, newComponent)) +} diff --git a/api/pkg/apis/v1alpha1/providers/providerfactory_test.go b/api/pkg/apis/v1alpha1/providers/providerfactory_test.go index d2ffd0751..f71e725ef 100644 --- a/api/pkg/apis/v1alpha1/providers/providerfactory_test.go +++ b/api/pkg/apis/v1alpha1/providers/providerfactory_test.go @@ -7,6 +7,7 @@ package providers import ( + "os" "testing" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" @@ -56,26 +57,40 @@ import ( ) func TestCreateProvider(t *testing.T) { + getTestMiniKubeEnabled := os.Getenv("TEST_MINIKUBE_ENABLED") + providerfactory := SymphonyProviderFactory{} provider, err := providerfactory.CreateProvider("providers.state.memory", memorystate.MemoryStateProviderConfig{}) assert.Nil(t, err) assert.NotNil(t, *provider.(*memorystate.MemoryStateProvider)) - provider, err = providerfactory.CreateProvider("providers.state.k8s", k8sstate.K8sStateProviderConfig{}) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8sstate.K8sStateProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping providers.state.k8s test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = providerfactory.CreateProvider("providers.state.k8s", k8sstate.K8sStateProviderConfig{}) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8sstate.K8sStateProvider)) + } - provider, err = providerfactory.CreateProvider("providers.config.k8scatalog", k8sstate.K8sStateProviderConfig{}) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8sstate.K8sStateProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping providers.config.k8scatalog test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = providerfactory.CreateProvider("providers.config.k8scatalog", k8sstate.K8sStateProviderConfig{}) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8sstate.K8sStateProvider)) + } provider, err = providerfactory.CreateProvider("providers.state.http", httpstate.HttpStateProviderConfig{Url: "http://localhost:3500/v1.0/state/statestore"}) assert.Nil(t, err) assert.NotNil(t, *provider.(*httpstate.HttpStateProvider)) - provider, err = providerfactory.CreateProvider("providers.reference.k8s", k8sref.K8sReferenceProviderConfig{}) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8sref.K8sReferenceProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping providers.reference.k8s test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = providerfactory.CreateProvider("providers.reference.k8s", k8sref.K8sReferenceProviderConfig{}) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8sref.K8sReferenceProvider)) + } provider, err = providerfactory.CreateProvider("providers.reference.customvision", cvref.CustomVisionReferenceProviderConfig{}) assert.Nil(t, err) @@ -85,9 +100,13 @@ func TestCreateProvider(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, *provider.(*httpref.HTTPReferenceProvider)) - provider, err = providerfactory.CreateProvider("providers.reporter.k8s", k8sreporter.K8sReporterConfig{}) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8sreporter.K8sReporter)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping providers.reporter.k8s test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = providerfactory.CreateProvider("providers.reporter.k8s", k8sreporter.K8sReporterConfig{}) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8sreporter.K8sReporter)) + } provider, err = providerfactory.CreateProvider("providers.reporter.http", httpreporter.HTTPReporterConfig{}) assert.Nil(t, err) @@ -117,21 +136,33 @@ func TestCreateProvider(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, *provider.(*adu.ADUTargetProvider)) - provider, err = providerfactory.CreateProvider("providers.target.k8s", k8s.K8sTargetProviderConfig{ConfigType: "path"}) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8s.K8sTargetProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping providers.target.k8s test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = providerfactory.CreateProvider("providers.target.k8s", k8s.K8sTargetProviderConfig{ConfigType: "path"}) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8s.K8sTargetProvider)) + } provider, err = providerfactory.CreateProvider("providers.target.docker", docker.DockerTargetProviderConfig{}) assert.Nil(t, err) assert.NotNil(t, *provider.(*docker.DockerTargetProvider)) - provider, err = providerfactory.CreateProvider("providers.target.ingress", ingress.IngressTargetProviderConfig{ConfigType: "path"}) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*ingress.IngressTargetProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping providers.target.ingress test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = providerfactory.CreateProvider("providers.target.ingress", ingress.IngressTargetProviderConfig{ConfigType: "path"}) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*ingress.IngressTargetProvider)) + } - provider, err = providerfactory.CreateProvider("providers.target.kubectl", kubectl.KubectlTargetProviderConfig{ConfigType: "path"}) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*kubectl.KubectlTargetProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping providers.target.kubectl test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = providerfactory.CreateProvider("providers.target.kubectl", kubectl.KubectlTargetProviderConfig{ConfigType: "path"}) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*kubectl.KubectlTargetProvider)) + } provider, err = providerfactory.CreateProvider("providers.target.staging", staging.StagingTargetProviderConfig{}) assert.Nil(t, err) @@ -161,9 +192,13 @@ func TestCreateProvider(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, *provider.(*tgtmock.MockTargetProvider)) - provider, err = providerfactory.CreateProvider("providers.target.configmap", configmap.ConfigMapTargetProviderConfig{ConfigType: "path"}) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*configmap.ConfigMapTargetProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping providers.target.configmap test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = providerfactory.CreateProvider("providers.target.configmap", configmap.ConfigMapTargetProviderConfig{ConfigType: "path"}) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*configmap.ConfigMapTargetProvider)) + } provider, err = providerfactory.CreateProvider("providers.config.mock", mockconfig.MockConfigProviderConfig{}) assert.Nil(t, err) @@ -231,6 +266,8 @@ func TestCreateProvider(t *testing.T) { } func TestCreateProviderForTargetRole(t *testing.T) { + getTestMiniKubeEnabled := os.Getenv("TEST_MINIKUBE_ENABLED") + targetState := model.TargetState{ Spec: &model.TargetSpec{ DisplayName: "target", @@ -526,21 +563,33 @@ func TestCreateProviderForTargetRole(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, *provider.(*memorystate.MemoryStateProvider)) - provider, err = CreateProviderForTargetRole(nil, "k8sstate", targetState, nil) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8sstate.K8sStateProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping k8sstate test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = CreateProviderForTargetRole(nil, "k8sstate", targetState, nil) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8sstate.K8sStateProvider)) + } - provider, err = CreateProviderForTargetRole(nil, "k8scatalog", targetState, nil) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8sstate.K8sStateProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping k8scatalog test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = CreateProviderForTargetRole(nil, "k8scatalog", targetState, nil) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8sstate.K8sStateProvider)) + } provider, err = CreateProviderForTargetRole(nil, "httpstate", targetState, nil) assert.Nil(t, err) assert.NotNil(t, *provider.(*httpstate.HttpStateProvider)) - provider, err = CreateProviderForTargetRole(nil, "k8sref", targetState, nil) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8sref.K8sReferenceProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping k8sref test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = CreateProviderForTargetRole(nil, "k8sref", targetState, nil) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8sref.K8sReferenceProvider)) + } provider, err = CreateProviderForTargetRole(nil, "cvref", targetState, nil) assert.Nil(t, err) @@ -558,21 +607,33 @@ func TestCreateProviderForTargetRole(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, *provider.(*counter.CounterStageProvider)) - provider, err = CreateProviderForTargetRole(nil, "k8s", targetState, nil) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8s.K8sTargetProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping k8s test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = CreateProviderForTargetRole(nil, "k8s", targetState, nil) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8s.K8sTargetProvider)) + } provider, err = CreateProviderForTargetRole(nil, "docker", targetState, nil) assert.Nil(t, err) assert.NotNil(t, *provider.(*docker.DockerTargetProvider)) - provider, err = CreateProviderForTargetRole(nil, "ingress", targetState, nil) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*ingress.IngressTargetProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping ingress test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = CreateProviderForTargetRole(nil, "ingress", targetState, nil) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*ingress.IngressTargetProvider)) + } - provider, err = CreateProviderForTargetRole(nil, "kubectl", targetState, nil) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*kubectl.KubectlTargetProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping kubectl test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = CreateProviderForTargetRole(nil, "kubectl", targetState, nil) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*kubectl.KubectlTargetProvider)) + } provider, err = CreateProviderForTargetRole(nil, "staging", targetState, nil) assert.Nil(t, err) @@ -598,9 +659,13 @@ func TestCreateProviderForTargetRole(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, *provider.(*proxy.ProxyUpdateProvider)) - provider, err = CreateProviderForTargetRole(nil, "configmap", targetState, nil) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*configmap.ConfigMapTargetProvider)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping configmap test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = CreateProviderForTargetRole(nil, "configmap", targetState, nil) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*configmap.ConfigMapTargetProvider)) + } provider, err = CreateProviderForTargetRole(nil, "mock", targetState, nil) assert.Nil(t, err) @@ -682,7 +747,11 @@ func TestCreateProviderForTargetRole(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, *provider.(*httpreporter.HTTPReporter)) - provider, err = CreateProviderForTargetRole(nil, "k8sreporter", targetState, nil) - assert.Nil(t, err) - assert.NotNil(t, *provider.(*k8sreporter.K8sReporter)) + if getTestMiniKubeEnabled == "" { + t.Log("Skipping k8sreporter test as TEST_MINIKUBE_ENABLED is not set") + } else { + provider, err = CreateProviderForTargetRole(nil, "k8sreporter", targetState, nil) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*k8sreporter.K8sReporter)) + } } diff --git a/api/pkg/apis/v1alpha1/providers/target/configmap/configmap.go b/api/pkg/apis/v1alpha1/providers/target/configmap/configmap.go index 005ec8120..de0525525 100644 --- a/api/pkg/apis/v1alpha1/providers/target/configmap/configmap.go +++ b/api/pkg/apis/v1alpha1/providers/target/configmap/configmap.go @@ -334,7 +334,7 @@ func (k *ConfigMapTargetProvider) ensureNamespace(ctx context.Context, namespace Name: namespace, }, }, metav1.CreateOptions{}) - if err != nil { + if err != nil && !kerrors.IsAlreadyExists(err) { sLog.Errorf(" P (ConfigMap Target): failed to create namespace: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) return err } diff --git a/api/pkg/apis/v1alpha1/providers/target/configmap/configmap_test.go b/api/pkg/apis/v1alpha1/providers/target/configmap/configmap_test.go index 1670676e1..ea2f3df2b 100644 --- a/api/pkg/apis/v1alpha1/providers/target/configmap/configmap_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/configmap/configmap_test.go @@ -92,6 +92,11 @@ func TestInitWithBadFile(t *testing.T) { } func TestInitWithEmptyConfigData(t *testing.T) { + getConfigMap := os.Getenv("TEST_CONFIGMAP") + if getConfigMap == "" { + t.Skip("Skipping because TEST_CONFIGMAP environment variable is not set") + } + config := ConfigMapTargetProviderConfig{ ConfigType: "path", ConfigData: "", @@ -159,7 +164,8 @@ func TestConfigMapTargetProviderApply(t *testing.T) { Namespace: "configs", }, Spec: &model.InstanceSpec{ - Name: "config-test", + Name: "config-test", + Scope: "configs", }, }, Solution: model.SolutionState{ @@ -180,8 +186,8 @@ func TestConfigMapTargetProviderApply(t *testing.T) { assert.Nil(t, err) } -// TestConfigMapTargetProviderDekete tests that deleting a configmap works -func TestConfigMapTargetProviderDekete(t *testing.T) { +// TestConfigMapTargetProviderGet tests that getting a configmap works +func TestConfigMapTargetProviderGet(t *testing.T) { getConfigMap := os.Getenv("TEST_CONFIGMAP") if getConfigMap == "" { t.Skip("Skipping because TEST_CONFIGMAP environment variable is not set") @@ -198,13 +204,6 @@ func TestConfigMapTargetProviderDekete(t *testing.T) { component := model.ComponentSpec{ Name: "test-config", Type: "config", - Properties: map[string]interface{}{ - "foo": "bar", - "complex": map[string]interface{}{ - "easy": "as", - "123": 456, - }, - }, } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ @@ -212,7 +211,8 @@ func TestConfigMapTargetProviderDekete(t *testing.T) { Namespace: "configs", }, Spec: &model.InstanceSpec{ - Name: "config-test", + Name: "config-test", + Scope: "configs", }, }, Solution: model.SolutionState{ @@ -224,17 +224,22 @@ func TestConfigMapTargetProviderDekete(t *testing.T) { step := model.DeploymentStep{ Components: []model.ComponentStep{ { - Action: model.ComponentDelete, + Action: model.ComponentUpdate, Component: component, }, }, } - _, err = provider.Apply(context.Background(), deployment, step, false) + components, err := provider.Get(context.Background(), deployment, step.Components) assert.Nil(t, err) + assert.Equal(t, 1, len(components)) + assert.Equal(t, "bar", components[0].Properties["foo"]) + assert.Equal(t, "as", components[0].Properties["complex"].(map[string]interface{})["easy"]) + // TODO: This could be problematic as integers are probably preferred + assert.Equal(t, 456.0, components[0].Properties["complex"].(map[string]interface{})["123"]) } -// TestConfigMapTargetProviderGet tests that getting a configmap works -func TestConfigMapTargetProviderGet(t *testing.T) { +// TestConfigMapTargetProviderDelete tests that deleting a configmap works +func TestConfigMapTargetProviderDelete(t *testing.T) { getConfigMap := os.Getenv("TEST_CONFIGMAP") if getConfigMap == "" { t.Skip("Skipping because TEST_CONFIGMAP environment variable is not set") @@ -251,6 +256,13 @@ func TestConfigMapTargetProviderGet(t *testing.T) { component := model.ComponentSpec{ Name: "test-config", Type: "config", + Properties: map[string]interface{}{ + "foo": "bar", + "complex": map[string]interface{}{ + "easy": "as", + "123": 456, + }, + }, } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ @@ -258,7 +270,8 @@ func TestConfigMapTargetProviderGet(t *testing.T) { Namespace: "configs", }, Spec: &model.InstanceSpec{ - Name: "config-test", + Name: "config-test", + Scope: "configs", }, }, Solution: model.SolutionState{ @@ -270,21 +283,20 @@ func TestConfigMapTargetProviderGet(t *testing.T) { step := model.DeploymentStep{ Components: []model.ComponentStep{ { - Action: model.ComponentUpdate, + Action: model.ComponentDelete, Component: component, }, }, } - components, err := provider.Get(context.Background(), deployment, step.Components) + _, err = provider.Apply(context.Background(), deployment, step, false) assert.Nil(t, err) - assert.Equal(t, 1, len(components)) - assert.Equal(t, "bar", components[0].Properties["foo"]) - assert.Equal(t, "as", components[0].Properties["complex"].(map[string]interface{})["easy"]) - // TODO: This could be problematic as integers are probably preferred - assert.Equal(t, 456.0, components[0].Properties["complex"].(map[string]interface{})["123"]) } func TestConfigMapTargetProviderApplyGetDelete(t *testing.T) { + getConfigMap := os.Getenv("TEST_CONFIGMAP") + if getConfigMap == "" { + t.Skip("Skipping because TEST_CONFIGMAP environment variable is not set") + } config := ConfigMapTargetProviderConfig{ InCluster: false, ConfigType: "path", @@ -309,7 +321,8 @@ func TestConfigMapTargetProviderApplyGetDelete(t *testing.T) { Namespace: "configs", }, Spec: &model.InstanceSpec{ - Name: "config-test", + Name: "config-test", + Scope: "configs", }, }, Solution: model.SolutionState{ diff --git a/api/pkg/apis/v1alpha1/providers/target/conformance/target_conformance.go b/api/pkg/apis/v1alpha1/providers/target/conformance/target_conformance.go index a32363064..b2a823c4e 100644 --- a/api/pkg/apis/v1alpha1/providers/target/conformance/target_conformance.go +++ b/api/pkg/apis/v1alpha1/providers/target/conformance/target_conformance.go @@ -113,7 +113,8 @@ func AnyRequiredPropertiesMissing[P target.ITargetProvider](t *testing.T, p P) { _, err := p.Apply(context.Background(), deployment, step, true) assert.NotNil(t, err) coaErr := err.(v1alpha2.COAError) - assert.Equal(t, v1alpha2.BadRequest, coaErr.State) + condition := coaErr.State == v1alpha2.BadRequest || coaErr.State == v1alpha2.ValidateFailed + assert.True(t, condition, "Expected coaErr.State to be either BadRequest or ValidateFailed, but got %v", coaErr.State) } } func ConformanceSuite[P target.ITargetProvider](t *testing.T, p P) { diff --git a/api/pkg/apis/v1alpha1/providers/target/docker/docker.go b/api/pkg/apis/v1alpha1/providers/target/docker/docker.go index d3a0bb919..63bc33d78 100644 --- a/api/pkg/apis/v1alpha1/providers/target/docker/docker.go +++ b/api/pkg/apis/v1alpha1/providers/target/docker/docker.go @@ -26,7 +26,9 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/logger" ) -var sLog = logger.NewLogger("coa.runtime") +const loggerName = "providers.target.docker" + +var sLog = logger.NewLogger(loggerName) type DockerTargetProviderConfig struct { Name string `json:"name"` @@ -207,7 +209,7 @@ func (i *DockerTargetProvider) Apply(ctx context.Context, deployment model.Deplo Status: v1alpha2.UpdateFailed, Message: err.Error(), } - sLog.Errorf(" P (Helm Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + sLog.Errorf(" P (Docker Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) return ret, err } diff --git a/api/pkg/apis/v1alpha1/providers/target/helm/auth.go b/api/pkg/apis/v1alpha1/providers/target/helm/auth.go new file mode 100644 index 000000000..32f3c39bc --- /dev/null +++ b/api/pkg/apis/v1alpha1/providers/target/helm/auth.go @@ -0,0 +1,127 @@ +package helm + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + cerrors "github.com/containerd/containerd/remotes/errors" + "helm.sh/helm/v3/pkg/registry" +) + +type ( + tokenExchangeResponse struct { + RefreshToken string `json:"refresh_token"` + } + + tokenExchangeRequest struct { + GrantType string `json:"grant_type"` + Service string `json:"service"` + AccessToken string `json:"access_token"` + } +) + +const ( + defaultAuthUser = "00000000-0000-0000-0000-000000000000" + defaultAzureScope = "https://management.azure.com/.default" + azureCrPostfix = ".azurecr.io" + exchangeURLFormat = "https://%s/oauth2/exchange" +) + +// loginToACR logs in to an Azure Container Registry using the provided helm registry client. +func loginToACR(host string) error { + client, err := registry.NewClient() + if err != nil { + sLog.Errorf("Failed to create registry client: %+v", err) + return err + } + + cred, err := azidentity.NewManagedIdentityCredential(nil) + if err != nil { + sLog.Errorf("failed to obtain a credential: %v", err) + return err + } + token, err := cred.GetToken(context.Background(), policy.TokenRequestOptions{ + Scopes: []string{defaultAzureScope}, + }) + + if err != nil { + sLog.Errorf("failed to get token: %v", err) + return err + } + + acrToken, err := exchangeToken(host, token.Token) + if err != nil { + sLog.Errorf("failed to exchange token: %v", err) + return err + } + + return client.Login(host, registry.LoginOptBasicAuth(defaultAuthUser, acrToken)) +} + +// isUnauthorized returns true if the error is an unauthorized error from the helm sdk. +func isUnauthorized(err error) bool { + if err == nil { + return false + } + var unexpectedStatusError = &cerrors.ErrUnexpectedStatus{} + if errors.As(err, unexpectedStatusError) { + return unexpectedStatusError.StatusCode == http.StatusUnauthorized + } + return false +} + +// isAzureContainerRegistry returns true if the host is an Azure Container Registry. +// This is a very rudimentary check that only checks for the .azurecr.io suffix. +func isAzureContainerRegistry(host string) bool { + return strings.HasSuffix(host, azureCrPostfix) +} + +func getHostFromOCIRef(ref string) (string, error) { + if !strings.HasPrefix(ref, "oci://") { + ref = fmt.Sprintf("oci://%s", ref) + } + parsed, err := url.Parse(ref) + if err != nil { + return "", err + } + + return parsed.Host, nil +} + +// exchangeToken exchanges an Azure AD token for an ACR refresh token. +// This is used by the Helm registry client to authenticate to ACR. +func exchangeToken(host, token string) (string, error) { + req := tokenExchangeRequest{ + GrantType: "access_token", + Service: host, + AccessToken: token, + } + + res := tokenExchangeResponse{} + + jsonResponse, err := http.PostForm(fmt.Sprintf(exchangeURLFormat, host), req.ToFormValues()) + if err != nil { + return "", err + } + + if err := json.NewDecoder(jsonResponse.Body).Decode(&res); err != nil { + return "", err + } + + return res.RefreshToken, nil +} + +func (r *tokenExchangeRequest) ToFormValues() url.Values { + return url.Values{ + "grant_type": {r.GrantType}, + "service": {r.Service}, + "access_token": {r.AccessToken}, + } +} diff --git a/api/pkg/apis/v1alpha1/providers/target/helm/helm.go b/api/pkg/apis/v1alpha1/providers/target/helm/helm.go index 927e907c5..ded5c3839 100644 --- a/api/pkg/apis/v1alpha1/providers/target/helm/helm.go +++ b/api/pkg/apis/v1alpha1/providers/target/helm/helm.go @@ -12,14 +12,17 @@ import ( "errors" "fmt" "io" - "io/ioutil" - "log" "net/http" "os" + "reflect" "strconv" "strings" + "time" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/metrics" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils/metahelper" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" @@ -31,18 +34,29 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/storage/driver" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) -var sLog = logger.NewLogger("coa.runtime") +var ( + sLog = logger.NewLogger(loggerName) + providerOperationMetrics *metrics.Metrics +) const ( - DEFAULT_NAMESPACE = "default" - TEMP_CHART_DIR = "/tmp/symphony/charts" + defaultNamespace = "default" + tempChartDir = "/tmp/symphony/charts" + helmDriver = "secret" + helm = "helm" + providerName = "P (Helm Target)" + loggerName = "providers.target.helm" ) type ( @@ -56,12 +70,9 @@ type ( } // HelmTargetProvider is the Helm provider HelmTargetProvider struct { - Config HelmTargetProviderConfig - Context *contexts.ManagerContext - ListClient *action.List - InstallClient *action.Install - UpgradeClient *action.Upgrade - UninstallClient *action.Uninstall + Config HelmTargetProviderConfig + Context *contexts.ManagerContext + MetaPopulator metahelper.MetaPopulator } // HelmProperty is the property for the Helm chart HelmProperty struct { @@ -71,6 +82,7 @@ type ( // HelmChartProperty is the property for the Helm Charts HelmChartProperty struct { Repo string `json:"repo"` + Name string `json:"name,omitempty"` Version string `json:"version"` Wait bool `json:"wait"` } @@ -113,7 +125,7 @@ func HelmTargetProviderConfigFromMap(properties map[string]string) (HelmTargetPr func (i *HelmTargetProvider) InitWithMap(properties map[string]string) error { config, err := HelmTargetProviderConfigFromMap(properties) if err != nil { - return err + return v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to init", providerName), v1alpha2.InitFailed) } return i.Init(config) @@ -136,9 +148,18 @@ func (i *HelmTargetProvider) Init(config providers.IProviderConfig) error { defer utils.CloseSpanWithError(span, &err) sLog.Info(" P (Helm Target): Init()") + i.MetaPopulator, err = metahelper.NewMetaPopulator(metahelper.WithDefaultPopulators()) + if err != nil { + sLog.Errorf(" P (Helm Target): failed to create meta populator: %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to create meta populator", providerName), v1alpha2.InitFailed) + sLog.Error(err) + return err + } + err = initChartsDir() if err != nil { sLog.Errorf(" P (Helm Target): failed to init charts dir: %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to init charts dir", providerName), v1alpha2.InitFailed) return err } @@ -147,18 +168,44 @@ func (i *HelmTargetProvider) Init(config providers.IProviderConfig) error { helmConfig, err = toHelmTargetProviderConfig(config) if err != nil { sLog.Errorf(" P (Helm Target): expected HelmTargetProviderConfig: %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to convert to HelmTargetProviderConfig", providerName), v1alpha2.InitFailed) return err } i.Config = helmConfig + + // validate config + _, err = i.createActionConfig(context.Background(), defaultNamespace) + if err != nil { + return err + } + + if providerOperationMetrics == nil { + providerOperationMetrics, err = metrics.New() + if err != nil { + return err + } + } + + return err +} + +func (i *HelmTargetProvider) createActionConfig(ctx context.Context, namespace string) (*action.Configuration, error) { var actionConfig *action.Configuration + if namespace == "" { + namespace = constants.DefaultScope + } + sLog.Debugf(" P (Helm Target): creating action config for namespace %s", namespace) + var err error if i.Config.InCluster { settings := cli.New() + settings.SetNamespace(namespace) actionConfig = new(action.Configuration) // TODO: $HELM_DRIVER set the backend storage driver. Values are: configmap, secret, memory, sql. Do we need to handle this differently? - if err = actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), log.Printf); err != nil { + if err = actionConfig.Init(settings.RESTClientGetter(), namespace, helmDriver, sLog.Debugf); err != nil { sLog.Errorf(" P (Helm Target): failed to init: %+v", err) - return err + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to init action config", providerName), v1alpha2.CreateActionConfigFailed) + return nil, err } } else { switch i.Config.ConfigType { @@ -167,34 +214,30 @@ func (i *HelmTargetProvider) Init(config providers.IProviderConfig) error { var kConfig *rest.Config kConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData)) if err != nil { - sLog.Errorf(" P (Helm Target): failed to init with config bytes: %+v", err) - return err + sLog.Errorf(" P (Helm Target): failed to get RestConfig: %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get RestConfig", providerName), v1alpha2.CreateActionConfigFailed) + return nil, err } - namespace := DEFAULT_NAMESPACE - actionConfig, err = getActionConfig(context.TODO(), namespace, kConfig) + actionConfig, err = getActionConfig(context.Background(), namespace, kConfig) if err != nil { - sLog.Errorf(" P (Helm Target): failed to init with config bytes: %+v", err) - return err + sLog.Errorf(" P (Helm Target): failed to get ActionConfig: %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get ActionConfig", providerName), v1alpha2.CreateActionConfigFailed) + return nil, err } } else { - err = v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig) - sLog.Errorf(" P (Helm Target): %+v", err) - return err + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: config data is not supplied", providerName), v1alpha2.CreateActionConfigFailed) + sLog.Error(err) + return nil, err } default: - err = v1alpha2.NewCOAError(nil, "unrecognized config type, accepted value is: bytes", v1alpha2.BadConfig) - sLog.Errorf(" P (Helm Target): %+v", err) - return err + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: unrecognized config type, accepted value is: bytes", providerName), v1alpha2.CreateActionConfigFailed) + sLog.Error(err) + return nil, err } } - - i.ListClient = action.NewList(actionConfig) - i.InstallClient = action.NewInstall(actionConfig) - i.UninstallClient = action.NewUninstall(actionConfig) - i.UpgradeClient = action.NewUpgrade(actionConfig) - return nil + return actionConfig, nil } // getActionConfig returns an action configuration @@ -205,11 +248,11 @@ func getActionConfig(ctx context.Context, namespace string, config *rest.Config) cliConfig.BearerToken = &config.BearerToken cliConfig.Namespace = &namespace // Drop their rest.Config and just return inject own - wrapper := func(*rest.Config) *rest.Config { + cliConfig.WithWrapConfigFn(func(*rest.Config) *rest.Config { return config - } - cliConfig.WithWrapConfigFn(wrapper) - if err := actionConfig.Init(cliConfig, namespace, "secret", log.Printf); err != nil { + }) + + if err := actionConfig.Init(cliConfig, namespace, helmDriver, sLog.Debugf); err != nil { return nil, err } @@ -238,13 +281,21 @@ func (i *HelmTargetProvider) Get(ctx context.Context, deployment model.Deploymen }, ) var err error + var actionConfig *action.Configuration defer utils.CloseSpanWithError(span, &err) sLog.Infof(" P (Helm Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) - i.ListClient.Deployed = true + actionConfig, err = i.createActionConfig(ctx, deployment.Instance.Spec.Scope) + if err != nil { + sLog.Error(err) + return nil, err + } + listClient := action.NewList(actionConfig) + listClient.Deployed = true var results []*release.Release - results, err = i.ListClient.Run() + results, err = listClient.Run() if err != nil { sLog.Errorf(" P (Helm Target): failed to create Helm list client: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to create Helm list client", providerName), v1alpha2.HelmActionFailed) return nil, err } @@ -287,11 +338,31 @@ func (*HelmTargetProvider) GetValidationRule(ctx context.Context) model.Validati OptionalMetadata: []string{}, ChangeDetectionProperties: []model.PropertyDesc{ {Name: "chart", IgnoreCase: false, SkipIfMissing: true}, //TODO: deep change detection on interface{} + {Name: "values", PropChanged: propChange}, }, }, } } +func propChange(old, new interface{}) bool { + // scenarios where either is an empty map and the other is nil count as no change + if isEmpty(old) && isEmpty(new) { + return false + } + return !reflect.DeepEqual(old, new) +} + +func isEmpty(values interface{}) bool { + if values == nil { + return true + } + valueMap, ok := values.(map[string]interface{}) + if ok { + return len(valueMap) == 0 + } + return false +} + // downloadFile will download a url to a local file. It's efficient because it will func downloadFile(url string, fileName string) error { resp, err := http.Get(url) @@ -323,10 +394,21 @@ func (i *HelmTargetProvider) Apply(ctx context.Context, deployment model.Deploym defer utils.CloseSpanWithError(span, &err) sLog.Infof(" P (Helm Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + functionName := utils.GetFunctionName() + applyTime := time.Now().UTC() components := step.GetComponents() err = i.GetValidationRule(ctx).Validate(components) if err != nil { sLog.Errorf(" P (Helm Target): failed to validate components: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + providerOperationMetrics.ProviderOperationErrors( + helm, + functionName, + metrics.ValidateRuleOperation, + metrics.CreateOperationType, + v1alpha2.ValidateFailed.String(), + ) + + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: the rule validation failed", providerName), v1alpha2.ValidateFailed) return nil, err } @@ -336,16 +418,40 @@ func (i *HelmTargetProvider) Apply(ctx context.Context, deployment model.Deploym ret := step.PrepareResultMap() + var actionConfig *action.Configuration + actionConfig, err = i.createActionConfig(ctx, deployment.Instance.Spec.Scope) + if err != nil { + sLog.Error(err) + providerOperationMetrics.ProviderOperationErrors( + helm, + functionName, + metrics.HelmActionConfigOperation, + metrics.CreateOperationType, + v1alpha2.CreateActionConfigFailed.String(), + ) + return ret, err + } + for _, component := range step.Components { + applyComponentTime := time.Now().UTC() if component.Action == model.ComponentUpdate { var helmProp *HelmProperty helmProp, err = getHelmPropertyFromComponent(component.Component) if err != nil { sLog.Errorf(" P (Helm Target): failed to get Helm properties: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get helm properties", providerName), v1alpha2.GetHelmPropertyFailed) ret[component.Component.Name] = model.ComponentResultSpec{ Status: v1alpha2.UpdateFailed, Message: err.Error(), } + providerOperationMetrics.ProviderOperationErrors( + helm, + functionName, + metrics.HelmPropertiesOperation, + metrics.GetOperationType, + v1alpha2.GetHelmPropertyFailed.String(), + ) + return ret, err } @@ -353,10 +459,19 @@ func (i *HelmTargetProvider) Apply(ctx context.Context, deployment model.Deploym fileName, err = i.pullChart(&helmProp.Chart) if err != nil { sLog.Errorf(" P (Helm Target): failed to pull chart: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to pull chart", providerName), v1alpha2.HelmActionFailed) ret[component.Component.Name] = model.ComponentResultSpec{ Status: v1alpha2.UpdateFailed, Message: err.Error(), } + providerOperationMetrics.ProviderOperationErrors( + helm, + functionName, + metrics.PullChartOperation, + metrics.UpdateOperationType, + v1alpha2.HelmChartPullFailed.String(), + ) + return ret, err } defer os.Remove(fileName) @@ -365,79 +480,172 @@ func (i *HelmTargetProvider) Apply(ctx context.Context, deployment model.Deploym chart, err = loader.Load(fileName) if err != nil { sLog.Errorf(" P (Helm Target): failed to load chart: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to load chart", providerName), v1alpha2.HelmActionFailed) ret[component.Component.Name] = model.ComponentResultSpec{ Status: v1alpha2.UpdateFailed, Message: err.Error(), } + providerOperationMetrics.ProviderOperationErrors( + helm, + functionName, + metrics.LoadChartOperation, + metrics.UpdateOperationType, + v1alpha2.HelmChartLoadFailed.String(), + ) + return ret, err } chart.Metadata.Tags = "SYM:" + helmProp.Chart.Repo //this is not used by Helm SDK, we use this to carry repo info - i.configureUpsertClients(component.Component.Name, &helmProp.Chart, &deployment) - if _, err = i.UpgradeClient.Run(component.Component.Name, chart, helmProp.Values); err != nil { - if _, err = i.InstallClient.Run(chart, helmProp.Values); err != nil { + postRender := &PostRenderer{ + instance: *deployment.Instance.Spec, + populator: i.MetaPopulator, + } + installClient := configureInstallClient(component.Component.Name, &helmProp.Chart, &deployment, actionConfig, postRender) + upgradeClient := configureUpgradeClient(&helmProp.Chart, &deployment, actionConfig, postRender) + + if _, err = upgradeClient.Run(component.Component.Name, chart, helmProp.Values); err != nil { + if _, err = installClient.Run(chart, helmProp.Values); err != nil { sLog.Errorf(" P (Helm Target): failed to apply: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to apply chart", providerName), v1alpha2.HelmActionFailed) ret[component.Component.Name] = model.ComponentResultSpec{ Status: v1alpha2.UpdateFailed, Message: err.Error(), } + providerOperationMetrics.ProviderOperationErrors( + helm, + functionName, + metrics.ApplyOperation, + metrics.UpdateOperationType, + v1alpha2.HelmChartApplyFailed.String(), + ) + return ret, err } } + ret[component.Component.Name] = model.ComponentResultSpec{ Status: v1alpha2.Updated, - Message: "", + Message: fmt.Sprintf("No error. %s has been updated", component.Component.Name), } + + providerOperationMetrics.ProviderOperationLatency( + applyComponentTime, + helm, + functionName, + metrics.ApplyOperation, + metrics.UpdateOperationType, + ) } else { - if component.Component.Type == "helm.v3" { - _, err = i.UninstallClient.Run(component.Component.Name) - if err != nil { - if strings.Contains(err.Error(), "not found") { - continue //TODO: better way to detect this error? - } + switch component.Component.Type { + case "helm.v3": + uninstallClient := configureUninstallClient(&deployment, actionConfig) + _, err = uninstallClient.Run(component.Component.Name) + if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { + sLog.Errorf(" P (Helm Target): failed to uninstall Helm chart: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to uninstall chart", providerName), v1alpha2.HelmActionFailed) ret[component.Component.Name] = model.ComponentResultSpec{ Status: v1alpha2.DeleteFailed, Message: err.Error(), } - sLog.Errorf(" P (Helm Target): failed to uninstall Helm chart: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + providerOperationMetrics.ProviderOperationErrors( + helm, + functionName, + metrics.HelmChartOperation, + metrics.UpdateOperationType, + v1alpha2.HelmChartUninstallFailed.String(), + ) + return ret, err } + ret[component.Component.Name] = model.ComponentResultSpec{ Status: v1alpha2.Deleted, Message: "", } + default: + sLog.Errorf(" P (Helm Target): Failed to apply as %v is an invalid helm version", component.Component.Type) } } + + providerOperationMetrics.ProviderOperationLatency( + applyComponentTime, + helm, + functionName, + metrics.ApplyOperation, + metrics.UpdateOperationType, + ) } + + providerOperationMetrics.ProviderOperationLatency( + applyTime, + helm, + functionName, + metrics.ApplyOperation, + metrics.UpdateOperationType, + ) + return ret, nil } func (i *HelmTargetProvider) pullChart(chart *HelmChartProperty) (fileName string, err error) { - fileName = fmt.Sprintf("%s/%s.tgz", TEMP_CHART_DIR, uuid.New().String()) + fileName = fmt.Sprintf("%s/%s.tgz", tempChartDir, uuid.New().String()) + + if strings.HasPrefix(chart.Repo, "http") { + var chartPath string + if strings.HasSuffix(chart.Repo, ".tgz") { + chartPath = chart.Repo + } else { + chartPath, err = repo.FindChartInRepoURL(chart.Repo, chart.Name, chart.Version, "", "", "", getter.All(&cli.EnvSettings{})) + if err != nil { + sLog.Errorf(" P (Helm Target): failed to find helm chart in repo: %+v", err) + return "", err + } + } - var pullRes *registry.PullResult - if strings.HasSuffix(chart.Repo, ".tgz") && strings.HasPrefix(chart.Repo, "http") { - err = downloadFile(chart.Repo, fileName) + err = downloadFile(chartPath, fileName) if err != nil { sLog.Errorf(" P (Helm Target): failed to download chart from repo: %+v", err) return "", err } } else { - var regClient *registry.Client - regClient, err = registry.NewClient() + var pullRes *registry.PullResult + + // Helm provider supports oci-based registry. Symphony manifest supports it in two formats. + // 1. with oci prefix, e.g. oci://myregistry.azurecr.io/mychart:1.0.0 (https://helm.sh/docs/topics/registries/#oci-feature-deprecation-and-behavior-changes-with-v370) + // 2. without oci prefix, e.g. myregistry.azurecr.io/mychart:1.0.0 (backwards compatibility with existing symphony behavior) + // However, registry.Client doesn't like the reference to be prefixed with "oci://" + // so we trim it here if it exists + pullRes, err = pullOCIChart(chart.Repo, chart.Version) if err != nil { - sLog.Errorf(" P (Helm Target): failed to create registry client: %+v", err) - return - } + sLog.Errorf(" P (Helm Target): got error pulling chart from repo: %+v", err) + host, herr := getHostFromOCIRef(chart.Repo) + if herr != nil { + sLog.Errorf(" P (Helm Target): failed to get host from oci ref: %+v", herr) + return "", herr + } + if isUnauthorized(err) && isAzureContainerRegistry(host) { + sLog.Infof(" P (Helm Target): artifact is hosted in ACR. Attempting to login to ACR") + err = loginToACR(host) + if err != nil { + sLog.Errorf(" P (Helm Target): failed to login to ACR: %+v", err) + return "", err + } + sLog.Infof(" P (Helm Target): successfully logged in to ACR. Now retrying to pull chart from repo") - pullRes, err = regClient.Pull(fmt.Sprintf("%s:%s", chart.Repo, chart.Version), registry.PullOptWithChart(true)) - if err != nil { - sLog.Errorf(" P (Helm Target): failed to pull chart from repo: %+v", err) - return + pullRes, err = pullOCIChart(chart.Repo, chart.Version) + if err != nil { + sLog.Errorf(" P (Helm Target): failed to pull chart from repo after login in: %+v", err) + return "", err + } + } else { + sLog.Errorf(" P (Helm Target): failed to pull chart from repo: %+v", err) + return + } } - err = ioutil.WriteFile(fileName, pullRes.Chart.Data, 0644) + err = os.WriteFile(fileName, pullRes.Chart.Data, 0644) if err != nil { sLog.Errorf(" P (Helm Target): failed to save chart: %+v", err) return @@ -446,22 +654,57 @@ func (i *HelmTargetProvider) pullChart(chart *HelmChartProperty) (fileName strin return fileName, nil } -func (i *HelmTargetProvider) configureUpsertClients(name string, componentProps *HelmChartProperty, deployment *model.DeploymentSpec) { +func pullOCIChart(repo, version string) (*registry.PullResult, error) { + client, err := registry.NewClient() + if err != nil { + sLog.Errorf(" P (Helm Target): failed to create registry client: %+v", err) + return nil, err + } + + pullRes, err := client.Pull(fmt.Sprintf("%s:%s", strings.TrimPrefix(repo, "oci://"), version), registry.PullOptWithChart(true)) + if err != nil { + return nil, err + } + + return pullRes, nil +} + +func configureInstallClient(name string, componentProps *HelmChartProperty, deployment *model.DeploymentSpec, config *action.Configuration, postRenderer postrender.PostRenderer) *action.Install { + installClient := action.NewInstall(config) + installClient.ReleaseName = name if deployment.Instance.Spec.Scope == "" { - i.InstallClient.Namespace = DEFAULT_NAMESPACE - i.UpgradeClient.Namespace = DEFAULT_NAMESPACE + installClient.Namespace = constants.DefaultScope } else { - i.InstallClient.Namespace = deployment.Instance.Spec.Scope - i.UpgradeClient.Namespace = deployment.Instance.Spec.Scope + installClient.Namespace = deployment.Instance.Spec.Scope } + installClient.Wait = componentProps.Wait + installClient.IsUpgrade = true + installClient.CreateNamespace = true + installClient.PostRenderer = postRenderer + // We can't add labels to the release in the current version of the helm client. + // This should added when we upgrade to helm ^3.13.1 + return installClient +} + +func configureUpgradeClient(componentProps *HelmChartProperty, deployment *model.DeploymentSpec, config *action.Configuration, postRenderer postrender.PostRenderer) *action.Upgrade { + upgradeClient := action.NewUpgrade(config) + upgradeClient.Wait = componentProps.Wait + if deployment.Instance.Spec.Scope == "" { + upgradeClient.Namespace = constants.DefaultScope + } else { + upgradeClient.Namespace = deployment.Instance.Spec.Scope + } + upgradeClient.ResetValues = true + upgradeClient.Install = true + upgradeClient.PostRenderer = postRenderer + // We can't add labels to the release in the current version of the helm client. + // This should added when we upgrade to helm ^3.13.1 + return upgradeClient +} - i.InstallClient.Wait = componentProps.Wait - i.UpgradeClient.Wait = componentProps.Wait - i.InstallClient.CreateNamespace = true - i.InstallClient.ReleaseName = name - i.InstallClient.IsUpgrade = true - i.UpgradeClient.Install = true - i.UpgradeClient.ResetValues = true +func configureUninstallClient(deployment *model.DeploymentSpec, config *action.Configuration) *action.Uninstall { + uninstallClient := action.NewUninstall(config) + return uninstallClient } func getHelmPropertyFromComponent(component model.ComponentSpec) (*HelmProperty, error) { @@ -488,8 +731,8 @@ func validateProps(props *HelmProperty) (*HelmProperty, error) { } func initChartsDir() error { - if _, err := os.Stat(TEMP_CHART_DIR); os.IsNotExist(err) { - err = os.MkdirAll(TEMP_CHART_DIR, os.ModePerm) + if _, err := os.Stat(tempChartDir); os.IsNotExist(err) { + err = os.MkdirAll(tempChartDir, os.ModePerm) if err != nil { return err } diff --git a/api/pkg/apis/v1alpha1/providers/target/helm/helm_test.go b/api/pkg/apis/v1alpha1/providers/target/helm/helm_test.go index 540d33f2f..f231307a4 100644 --- a/api/pkg/apis/v1alpha1/providers/target/helm/helm_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/helm/helm_test.go @@ -1,17 +1,19 @@ /* - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * SPDX-License-Identifier: MIT +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT license. +* SPDX-License-Identifier: MIT */ package helm import ( "context" + "fmt" "net/http" "net/http/httptest" "os" "testing" + "time" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/conformance" @@ -19,6 +21,12 @@ import ( "k8s.io/client-go/rest" ) +const ( + bluefinRepo = "azbluefin.azurecr.io/helm/bluefin-arc-extension" + bluefinVersion = "0.2.0-20230717.3-develop" + defaultTestScope = "alice-springs" +) + // TestHelmTargetProviderConfigFromMapNil tests the HelmTargetProviderConfigFromMap function with nil input func TestHelmTargetProviderConfigFromMapNil(t *testing.T) { _, err := HelmTargetProviderConfigFromMap(nil) @@ -97,9 +105,8 @@ func TestHelmTargetProviderGetHelmProperty(t *testing.T) { Type: "helm.v3", Properties: map[string]interface{}{ "chart": map[string]string{ - "repo": "azbluefin.azurecr.io/helmcharts/bluefin-arc-extension/bluefin-arc-extension", - "name": "bluefin-arc-extension", - "version": "0.1.1", + "repo": bluefinRepo, + "version": bluefinVersion, }, "values": map[string]interface{}{ "CUSTOM_VISION_KEY": "BBB", @@ -118,47 +125,63 @@ func TestHelmTargetProviderInstall(t *testing.T) { if testSymphonyHelmVersion == "" { t.Skip("Skipping because TEST_SYMPHONY_HELM_VERSION environment variable is not set") } - - config := HelmTargetProviderConfig{InCluster: true} - provider := HelmTargetProvider{} - err := provider.Init(config) - assert.Nil(t, err) - component := model.ComponentSpec{ - Name: "bluefin-arc-extensions", - Type: "helm.v3", - Properties: map[string]interface{}{ - "chart": map[string]string{ - "repo": "azbluefin.azurecr.io/helmcharts/bluefin-arc-extension/bluefin-arc-extension", - "name": "bluefin-arc-extension", - "version": "0.1.1", - }, - "values": map[string]interface{}{ - "CUSTOM_VISION_KEY": "BBB", - "CLUSTER_SECRET": "test", - "CERTIFICATES": []string{"a", "b"}, - }, - }, - } - deployment := model.DeploymentSpec{ - Instance: model.InstanceState{ - Spec: &model.InstanceSpec{}, - }, - Solution: model.SolutionState{ - Spec: &model.SolutionSpec{ - Components: []model.ComponentSpec{component}, - }, - }, - } - step := model.DeploymentStep{ - Components: []model.ComponentStep{ - { - Action: model.ComponentUpdate, - Component: component, - }, - }, + testCases := []struct { + Name string + ChartRepo string + ExpectedError bool + }{ + {Name: "install with wrong protocol", ChartRepo: fmt.Sprintf("wrongproto://%s", bluefinRepo), ExpectedError: true}, + {Name: "install with oci prefix", ChartRepo: fmt.Sprintf("oci://%s", bluefinRepo), ExpectedError: false}, + {Name: "install without oci prefix", ChartRepo: bluefinRepo, ExpectedError: false}, + // cleanup step is in TestHelmTargetProviderRemove + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + config := HelmTargetProviderConfig{InCluster: true} + provider := HelmTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + component := model.ComponentSpec{ + Name: "bluefin-arc-extensions", + Type: "helm.v3", + Properties: map[string]interface{}{ + "chart": map[string]string{ + "repo": tc.ChartRepo, + "version": bluefinVersion, + }, + "values": map[string]interface{}{ + "CUSTOM_VISION_KEY": "BBB", + "CLUSTER_SECRET": "test", + "CERTIFICATES": []string{"a", "b"}, + }, + }, + } + deployment := model.DeploymentSpec{ + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{ + Scope: defaultTestScope, + Name: "test-instance", + }, + }, + } + step := model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Equal(t, tc.ExpectedError, err != nil, "[TestCase: %s] failed. ExpectedError: %s", tc.Name, tc.ExpectedError) + }) } - _, err = provider.Apply(context.Background(), deployment, step, false) - assert.Nil(t, err) } // TestHelmTargetProviderGet tests the Get function of HelmTargetProvider @@ -175,7 +198,10 @@ func TestHelmTargetProviderGet(t *testing.T) { assert.Nil(t, err) components, err := provider.Get(context.Background(), model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{}, + Spec: &model.InstanceSpec{ + Scope: defaultTestScope, + Name: "test-instance", + }, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -198,54 +224,124 @@ func TestHelmTargetProviderGet(t *testing.T) { assert.Equal(t, 1, len(components)) } -// TestHelmTargetProviderInstallNoOci tests the Apply function of HelmTargetProvider with no OCI registry -func TestHelmTargetProviderInstallNoOci(t *testing.T) { +// TestHelmTargetProvider_NonOciChart tests the Apply function of HelmTargetProvider with no OCI registry +func TestHelmTargetProvider_NonOciChart(t *testing.T) { // To run this test case successfully, you shouldn't have a symphony Helm chart already deployed to your current Kubernetes context - testSymphonyHelmVersion := os.Getenv("TEST_SYMPHONY_HELM_VERSIONS") + testSymphonyHelmVersion := os.Getenv("TEST_SYMPHONY_HELM_VERSION") if testSymphonyHelmVersion == "" { t.Skip("Skipping because TEST_SYMPHONY_HELM_VERSION environment variable is not set") } - config := HelmTargetProviderConfig{InCluster: true} - provider := HelmTargetProvider{} - err := provider.Init(config) - assert.Nil(t, err) - component := model.ComponentSpec{ - Name: "akri", - Type: "helm.v3", - Properties: map[string]interface{}{ - "chart": map[string]string{ - "repo": "https://project-akri.github.io/akri/akri", - "name": "akri", + testCases := []struct { + Name string + Chart map[string]string + Action model.ComponentAction + ExpectedError bool + }{ + { + Name: "repo URL not found ", + Chart: map[string]string{ + "repo": "https://not-found", + "name": "", "version": "", }, + Action: model.ComponentUpdate, + ExpectedError: true, }, - } - deployment := model.DeploymentSpec{ - Instance: model.InstanceState{ - Spec: &model.InstanceSpec{}, + { + Name: "chart not found in repo", + Chart: map[string]string{ + "repo": "https://project-akri.github.io/akri", + "name": "akri-not-found", + "version": "", + }, + Action: model.ComponentUpdate, + ExpectedError: true, }, - Solution: model.SolutionState{ - Spec: &model.SolutionSpec{ - Components: []model.ComponentSpec{component}, + { + Name: "version not found in repo", + Chart: map[string]string{ + "repo": "https://project-akri.github.io/akri", + "name": "akri", + "version": "0.0.0", }, + Action: model.ComponentUpdate, + ExpectedError: true, }, - } - step := model.DeploymentStep{ - Components: []model.ComponentStep{ - { - Action: model.ComponentUpdate, - Component: component, + { + Name: "update valid configuration without version", + Chart: map[string]string{ + "repo": "https://project-akri.github.io/akri", + "name": "akri", + "version": "", }, + Action: model.ComponentUpdate, + ExpectedError: false, }, + { + Name: "update valid configuration with version", + Chart: map[string]string{ + "repo": "https://project-akri.github.io/akri", + "name": "akri", + "version": "0.12.9", + }, + Action: model.ComponentUpdate, + ExpectedError: false, + }, + { + Name: "delete non-oci chart", + Chart: map[string]string{ + "repo": "https://project-akri.github.io/akri", + "name": "akri", + "version": "0.12.9", + }, + Action: model.ComponentDelete, + ExpectedError: false, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + config := HelmTargetProviderConfig{InCluster: true} + provider := HelmTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + component := model.ComponentSpec{ + Name: "akri", + Type: "helm.v3", + Properties: map[string]interface{}{ + "chart": tc.Chart, + }, + } + deployment := model.DeploymentSpec{ + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{ + Scope: defaultTestScope, + Name: "test-instance-no-oci", + }, + }, + } + step := model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: tc.Action, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Equal(t, tc.ExpectedError, err != nil, "[chart %s]: %s failed. ExpectedError: %s", tc.Action, tc.Name, tc.ExpectedError) + }) } - _, err = provider.Apply(context.Background(), deployment, step, false) - assert.Nil(t, err) } func TestHelmTargetProviderInstallNginxIngress(t *testing.T) { // To run this test case successfully, you shouldn't have a symphony Helm chart already deployed to your current Kubernetes context - testSymphonyHelmVersion := os.Getenv("TEST_SYMPHONY_HELM_VERSIONS") + testSymphonyHelmVersion := os.Getenv("TEST_SYMPHONY_HELM_VERSION") if testSymphonyHelmVersion == "" { t.Skip("Skipping because TEST_SYMPHONY_HELM_VERSION environment variable is not set") } @@ -310,6 +406,19 @@ func TestHelmTargetProviderInstallNginxIngress(t *testing.T) { } _, err = provider.Apply(context.Background(), deployment, step, false) assert.Nil(t, err) + + time.Sleep(3 * time.Second) + // cleanup + step = model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentDelete, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) } // TestHelmTargetProviderInstallDirectDownload tests the Apply function of HelmTargetProvider with direct download @@ -324,18 +433,21 @@ func TestHelmTargetProviderInstallDirectDownload(t *testing.T) { err := provider.Init(config) assert.Nil(t, err) component := model.ComponentSpec{ - Name: "gatekeeper", + Name: "hello-world", Type: "helm.v3", Properties: map[string]interface{}{ "chart": map[string]string{ - "repo": "https://open-policy-agent.github.io/gatekeeper/charts/gatekeeper-3.10.0-beta.1.tgz", - "name": "gatekeeper", + "repo": "https://github.com/helm/examples/releases/download/hello-world-0.1.0/hello-world-0.1.0.tgz", + "name": "hello-world", }, }, } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{}, + Spec: &model.InstanceSpec{ + Scope: defaultTestScope, + Name: "test-instance", + }, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -371,16 +483,16 @@ func TestHelmTargetProviderRemove(t *testing.T) { Type: "helm.v3", Properties: map[string]interface{}{ "chart": map[string]string{ - "repo": "azbluefin.azurecr.io/helmcharts/bluefin-arc-extension/bluefin-arc-extension", - "name": "bluefin-arc-extension", - "version": "0.1.1", + "repo": bluefinRepo, + "version": bluefinVersion, }, }, } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ Spec: &model.InstanceSpec{ - Name: "symphony", + Scope: defaultTestScope, + Name: "test-instance", }, }, Solution: model.SolutionState{ @@ -413,25 +525,25 @@ func TestHelmTargetProviderGetAnotherCluster(t *testing.T) { InCluster: false, ConfigType: "bytes", ConfigData: `apiVersion: v1 -clusters: -- cluster: - certificate-authority-data: ... - server: https://k12s-dns-6b5afdc5.hcp.westus3.azmk8s.io:443 - name: k12s -contexts: -- context: - cluster: k12s - user: clusterUser_symphony_k12s - name: k12s -current-context: k12s -kind: Config -preferences: {} -users: -- name: clusterUser_symphony_k12s - user: - client-certificate-data: ... - client-key-data: ... - token: ...`, + clusters: + - cluster: + certificate-authority-data: ... + server: https://k12s-dns-6b5afdc5.hcp.westus3.azmk8s.io:443 + name: k12s + contexts: + - context: + cluster: k12s + user: clusterUser_symphony_k12s + name: k12s + current-context: k12s + kind: Config + preferences: {} + users: + - name: clusterUser_symphony_k12s + user: + client-certificate-data: ... + client-key-data: ... + token: ...`, } provider := HelmTargetProvider{} err := provider.Init(config) @@ -606,3 +718,26 @@ func TestConformanceSuite(t *testing.T) { assert.Nil(t, err) conformance.ConformanceSuite(t, provider) } + +func TestPropChange(t *testing.T) { + type PropertyChangeCase struct { + Name string + OldProp map[string]interface{} + NewProp map[string]interface{} + Changed bool + } + var cases = []PropertyChangeCase{ + {"Both nil", nil, nil, false}, + {"Old empty New nil", map[string]interface{}{}, nil, false}, + {"Old nil New empty", nil, map[string]interface{}{}, false}, + {"No change", map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "b"}, false}, + {"Balue changed", map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "c"}, true}, + {"New property added", map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "b", "c": "d"}, true}, + {"Property removed", map[string]interface{}{"a": "b", "c": "d"}, map[string]interface{}{"a": "b"}, true}, + {"Property order changed", map[string]interface{}{"a": "b", "c": "d"}, map[string]interface{}{"c": "d", "a": "b"}, false}, + } + + for _, c := range cases { + assert.Equal(t, c.Changed, propChange(c.OldProp, c.NewProp), c.Name) + } +} diff --git a/api/pkg/apis/v1alpha1/providers/target/helm/postrenderer.go b/api/pkg/apis/v1alpha1/providers/target/helm/postrenderer.go new file mode 100644 index 000000000..2fd4ed488 --- /dev/null +++ b/api/pkg/apis/v1alpha1/providers/target/helm/postrenderer.go @@ -0,0 +1,132 @@ +package helm + +import ( + "bufio" + "bytes" + "errors" + "io" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils/metahelper" + "helm.sh/helm/v3/pkg/postrender" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + serializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + "k8s.io/apimachinery/pkg/util/yaml" + syaml "sigs.k8s.io/yaml" +) + +type ( + PostRenderer struct { + populator metahelper.MetaPopulator + instance model.InstanceSpec + } + encodable interface{ UnstructuredContent() map[string]interface{} } +) + +var ( + decoder = serializer.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + + _ postrender.PostRenderer = &PostRenderer{} +) + +// Run implements PostRenderer. +func (r *PostRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { + modifiedManifests = new(bytes.Buffer) + reader := yaml.NewYAMLReader(bufio.NewReader(renderedManifests)) + + for { + // Read the next YAML document + manifest, err := reader.Read() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, err + } + // Skip empty manifests + if len(manifest) == 0 { + continue + } + // Source is the first line of the manifest. + modifiedManifests.WriteString("---\n") + modifiedManifests.Write(r.getSourceBytes(manifest)) + + // Decode the manifest into a Kubernetes resource + obj, _, err := decoder.Decode(manifest, nil, nil) + if err != nil { + return nil, err + } + + // Apply the metadata to the resource + if err := r.populateMeta(obj); err != nil { + return nil, err + } + + // Re-encode the manifest and write it to the modifiedManifests buffer + // We won't use the codec because it encodes in JSON, not YAML + if err := r.encodeInto(modifiedManifests, obj); err != nil { + return nil, err + } + + if err != nil { + return nil, err + } + } + syaml.Marshal(modifiedManifests) + + return modifiedManifests, nil +} + +func (r *PostRenderer) populateMeta(obj runtime.Object) error { + switch obj := obj.(type) { + case *unstructured.Unstructured: + if err := r.populator.PopulateMeta(obj, r.instance); err != nil { + return err + } + case *unstructured.UnstructuredList: + for _, item := range obj.Items { + if err := r.populator.PopulateMeta(&item, r.instance); err != nil { + return err + } + } + default: + return errors.New("manifest is not a Kubernetes resource") + } + return nil +} + +func (r *PostRenderer) encodeInto(buffer io.Writer, obj runtime.Object) error { + switch obj := obj.(type) { + case *unstructured.Unstructured: + if err := encodeInto(buffer, obj); err != nil { + return err + } + case *unstructured.UnstructuredList: + if err := encodeInto(buffer, obj); err != nil { + return err + } + default: + return errors.New("manifest is not a Kubernetes resource") + } + return nil +} + +func encodeInto(writer io.Writer, obj encodable) (err error) { + bytes, err := syaml.Marshal(obj.UnstructuredContent()) + if err != nil { + return err + } + _, err = writer.Write(bytes) + return err +} + +func (r *PostRenderer) getSourceBytes(manifest []byte) []byte { + // the very first manifest usually starts with "---\n" + // we need to remove it to get the source + var startIndex int + if bytes.HasPrefix(manifest, []byte("---\n")) { + startIndex = 4 + } + return manifest[startIndex : bytes.IndexByte(manifest[startIndex:], '\n')+1+startIndex] +} diff --git a/api/pkg/apis/v1alpha1/providers/target/http/http.go b/api/pkg/apis/v1alpha1/providers/target/http/http.go index d73879e5b..a1f588816 100644 --- a/api/pkg/apis/v1alpha1/providers/target/http/http.go +++ b/api/pkg/apis/v1alpha1/providers/target/http/http.go @@ -23,7 +23,9 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/logger" ) -var sLog = logger.NewLogger("coa.runtime") +const loggerName = "providers.target.http" + +var sLog = logger.NewLogger(loggerName) type HttpTargetProviderConfig struct { Name string `json:"name"` @@ -179,6 +181,11 @@ func (i *HttpTargetProvider) Apply(ctx context.Context, deployment model.Deploym sLog.Errorf(" P (HTTP Target): %v, traceId: %s", err, span.SpanContext().TraceID().String()) return ret, err } + + ret[component.Component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.Updated, + Message: "HTTP request succeeded", + } } } return ret, nil diff --git a/api/pkg/apis/v1alpha1/providers/target/ingress/ingress.go b/api/pkg/apis/v1alpha1/providers/target/ingress/ingress.go index e4d16c7d7..ce6435edf 100644 --- a/api/pkg/apis/v1alpha1/providers/target/ingress/ingress.go +++ b/api/pkg/apis/v1alpha1/providers/target/ingress/ingress.go @@ -346,7 +346,7 @@ func (k *IngressTargetProvider) ensureNamespace(ctx context.Context, namespace s Name: namespace, }, }, metav1.CreateOptions{}) - if err != nil { + if err != nil && !kerrors.IsAlreadyExists(err) { sLog.Errorf(" P (Ingress Target): failed to create namespace: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) return err } diff --git a/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go b/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go index 602850a76..85c0aab81 100644 --- a/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go +++ b/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go @@ -9,7 +9,6 @@ package k8s import ( "context" "encoding/json" - "errors" "fmt" "path/filepath" "strconv" @@ -37,14 +36,17 @@ import ( "k8s.io/client-go/util/homedir" ) -var log = logger.NewLogger("coa.runtime") +const loggerName = "providers.target.k8s" + +var log = logger.NewLogger(loggerName) const ( - ENV_NAME string = "SYMPHONY_AGENT_ADDRESS" - SINGLE_POD string = "single-pod" - SERVICES string = "services" - SERVICES_NS string = "ns-services" - SERVICES_HNS string = "hns-services" //TODO: future versions + ENV_NAME string = "SYMPHONY_AGENT_ADDRESS" + SINGLE_POD string = "single-pod" + SERVICES string = "services" + SERVICES_NS string = "ns-services" + SERVICES_HNS string = "hns-services" //TODO: future versions + componentName string = "P (K8s Target Provider)" ) type K8sTargetProviderConfig struct { @@ -135,7 +137,7 @@ func K8sTargetProviderConfigFromMap(properties map[string]string) (K8sTargetProv func (i *K8sTargetProvider) InitWithMap(properties map[string]string) error { config, err := K8sTargetProviderConfigFromMap(properties) if err != nil { - return err + return v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to init", componentName), v1alpha2.InitFailed) } return i.Init(config) } @@ -158,59 +160,63 @@ func (i *K8sTargetProvider) Init(config providers.IProviderConfig) error { updateConfig, err := toK8sTargetProviderConfig(config) if err != nil { log.Errorf(" P (K8s Target): expected K8sTargetProviderConfig - %+v", err) - return errors.New("expected K8sTargetProviderConfig") + return v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to convert to K8sTargetProviderConfig", componentName), v1alpha2.InitFailed) } i.Config = updateConfig var kConfig *rest.Config - if i.Config.InCluster { - kConfig, err = rest.InClusterConfig() - } else { - switch i.Config.ConfigType { - case "path": - if i.Config.ConfigData == "" { - if home := homedir.HomeDir(); home != "" { - i.Config.ConfigData = filepath.Join(home, ".kube", "config") - } else { - err = v1alpha2.NewCOAError(nil, "can't locate home direction to read default kubernetes config file, to run in cluster, set inCluster config setting to true", v1alpha2.BadConfig) - log.Errorf(" P (K8s Target): %+v", err) - return err - } - } - kConfig, err = clientcmd.BuildConfigFromFlags("", i.Config.ConfigData) - case "bytes": - if i.Config.ConfigData != "" { - kConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData)) - if err != nil { - log.Errorf(" P (K8s Target): failed to get RESTconfg: %+v", err) - return err - } - } else { - err = v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig) - log.Errorf(" P (K8s Target): %+v", err) - return err - } - default: - err = v1alpha2.NewCOAError(nil, "unrecognized config type, accepted values are: path and inline", v1alpha2.BadConfig) - log.Errorf(" P (K8s Target): %+v", err) - return err - } - } + kConfig, err = i.getKubernetesConfig() if err != nil { log.Errorf(" P (K8s Target): failed to get the cluster config: %+v", err) - return err + return v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get kubernetes config", componentName), v1alpha2.InitFailed) } + i.Client, err = kubernetes.NewForConfig(kConfig) if err != nil { log.Errorf(" P (K8s Target): failed to create a new clientset: %+v", err) - return err + return v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to create kubernetes client", componentName), v1alpha2.InitFailed) } + i.DynamicClient, err = dynamic.NewForConfig(kConfig) if err != nil { log.Errorf(" P (K8s Target): failed to create a discovery client: %+v", err) - return err + return v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to create dynamic client", componentName), v1alpha2.InitFailed) } return nil } + +func (i *K8sTargetProvider) getKubernetesConfig() (*rest.Config, error) { + if i.Config.InCluster { + return rest.InClusterConfig() + } + + switch i.Config.ConfigType { + case "path": + return i.getConfigFromPath() + case "bytes": + return i.getConfigFromBytes() + default: + return nil, v1alpha2.NewCOAError(nil, "unrecognized config type, accepted values are: path and bytes", v1alpha2.BadConfig) + } +} + +func (i *K8sTargetProvider) getConfigFromPath() (*rest.Config, error) { + if i.Config.ConfigData == "" { + home := homedir.HomeDir() + if home == "" { + return nil, v1alpha2.NewCOAError(nil, "can't locate home directory to read default kubernetes config file. To run in cluster, set inCluster to true", v1alpha2.BadConfig) + } + i.Config.ConfigData = filepath.Join(home, ".kube", "config") + } + return clientcmd.BuildConfigFromFlags("", i.Config.ConfigData) +} + +func (i *K8sTargetProvider) getConfigFromBytes() (*rest.Config, error) { + if i.Config.ConfigData == "" { + return nil, v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig) + } + return clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData)) +} + func toK8sTargetProviderConfig(config providers.IProviderConfig) (K8sTargetProviderConfig, error) { ret := K8sTargetProviderConfig{} data, err := json.Marshal(config) @@ -293,6 +299,7 @@ func (i *K8sTargetProvider) Get(ctx context.Context, dep model.DeploymentSpec, r components, err = i.getDeployment(ctx, dep.Instance.Spec.Scope, dep.Instance.Spec.Name) if err != nil { log.Debugf(" P (K8s Target Provider): failed to get - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get components from deployment spec", componentName), v1alpha2.GetComponentSpecFailed) return nil, err } case SERVICES, SERVICES_NS: @@ -307,11 +314,12 @@ func (i *K8sTargetProvider) Get(ctx context.Context, dep model.DeploymentSpec, r cComponents, err = i.getDeployment(ctx, scope, component.Name) if err != nil { log.Debugf(" P (K8s Target Provider): failed to get deployment: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get components from deployment spec", componentName), v1alpha2.GetComponentSpecFailed) return nil, err } if len(cComponents) > 1 { log.Debugf(" P (K8s Target Provider): can't read multiple components %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) - err = v1alpha2.NewCOAError(nil, fmt.Sprintf("can't read multiple components when %s strategy or %s strategy is used", SERVICES, SERVICES_NS), v1alpha2.InternalError) + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: can't read multiple components when %s strategy or %s strategy is used", componentName, SERVICES, SERVICES_NS), v1alpha2.GetComponentSpecFailed) return nil, err } if len(cComponents) == 1 { @@ -329,6 +337,7 @@ func (i *K8sTargetProvider) Get(ctx context.Context, dep model.DeploymentSpec, r err = i.fillServiceMeta(ctx, scope, serviceName, cComponents[0]) if err != nil { log.Debugf(" P (K8s Target Provider): failed to get: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to fill service meta data", componentName), v1alpha2.GetComponentSpecFailed) return nil, err } components = append(components, cComponents...) @@ -648,6 +657,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, err = i.GetValidationRule(ctx).Validate(components) if err != nil { log.Errorf(" P (K8s Target Provider): failed to validate components, error: %v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: the rule validation failed", componentName), v1alpha2.ValidateFailed) return nil, err } if isDryRun { @@ -659,6 +669,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, projector, err := createProjector(i.Config.Projector) if err != nil { log.Debugf(" P (K8s Target Provider): failed to create projector: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to create projector", componentName), v1alpha2.CreateProjectorFailed) return ret, err } @@ -669,6 +680,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, err = i.deployComponents(ctx, span, dep.Instance.Spec.Scope, dep.Instance.Spec.Name, dep.Instance.Spec.Metadata, components, projector, dep.Instance.Spec.Name) if err != nil { log.Debugf(" P (K8s Target Provider): failed to apply components: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to deploy components", componentName), v1alpha2.K8sDeploymentFailed) return ret, err } } @@ -681,11 +693,13 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, err = i.removeService(ctx, dep.Instance.Spec.Scope, serviceName) if err != nil { log.Debugf(" P (K8s Target Provider): failed to remove service: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to remove k8s service", componentName), v1alpha2.K8sRemoveServiceFailed) return ret, err } err = i.removeDeployment(ctx, dep.Instance.Spec.Scope, dep.Instance.Spec.Name) if err != nil { log.Debugf(" P (K8s Target Provider): failed to remove deployment: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to remove k8s deployment", componentName), v1alpha2.K8sRemoveDeploymentFailed) return ret, err } if i.Config.DeleteEmptyNamespace { @@ -714,6 +728,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, err = i.deployComponents(ctx, span, scope, component.Name, component.Metadata, []model.ComponentSpec{component}, projector, dep.Instance.Spec.Name) if err != nil { log.Debugf(" P (K8s Target Provider): failed to apply components: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to deploy components", componentName), v1alpha2.K8sDeploymentFailed) return ret, err } } @@ -738,6 +753,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, Message: err.Error(), } log.Debugf("P (K8s Target Provider): failed to remove service: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to remove k8s service", componentName), v1alpha2.K8sRemoveServiceFailed) return ret, err } err = i.removeDeployment(ctx, scope, component.Name) @@ -747,6 +763,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, Message: err.Error(), } log.Debugf("P (K8s Target Provider): failed to remove deployment: %s, traceId: %", err.Error(), span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to remove k8s deployment", componentName), v1alpha2.K8sRemoveDeploymentFailed) return ret, err } if i.Config.DeleteEmptyNamespace { diff --git a/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl.go b/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl.go index 460c1d625..bb01c31da 100644 --- a/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl.go +++ b/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl.go @@ -12,18 +12,24 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" "path/filepath" "strconv" + "time" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/metrics" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils/metahelper" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/logger" + "github.com/oliveagle/jsonpath" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -42,8 +48,19 @@ import ( ) var ( - decUnstructured = yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) - sLog = logger.NewLogger("coa.runtime") + decUnstructured = yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + sLog = logger.NewLogger(loggerName) + providerOperationMetrics *metrics.Metrics +) + +const ( + kubectl = "kubectl" + timeout = "5m" + interval = "5s" + initialWait = "1m" + + providerName = "P (Kubectl Target)" + loggerName = "providers.target.kubectl" ) type ( @@ -65,6 +82,18 @@ type ( DiscoveryClient *discovery.DiscoveryClient Mapper *restmapper.DeferredDiscoveryRESTMapper RESTConfig *rest.Config + MetaPopulator metahelper.MetaPopulator + } + + // StatusProbe is the expected resource status property + StatusProbe struct { + SucceededValues []string `json:"succeededValues,omitempty"` + FailedValues []string `json:"failedValues,omitempty"` + StatusPath string `json:"statusPath,omitempty"` + ErrorMessagePath string `json:"errorMessagePath,omitempty"` + Timeout string `json:"timeout,omitempty"` + Interval string `json:"interval,omitempty"` + InitialWait string `json:"initialWait,omitempty"` } ) @@ -100,7 +129,7 @@ func KubectlTargetProviderConfigFromMap(properties map[string]string) (KubectlTa func (i *KubectlTargetProvider) InitWithMap(properties map[string]string) error { config, err := KubectlTargetProviderConfigFromMap(properties) if err != nil { - return err + return v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to init", providerName), v1alpha2.InitFailed) } return i.Init(config) @@ -126,72 +155,101 @@ func (i *KubectlTargetProvider) Init(config providers.IProviderConfig) error { updateConfig, err := toKubectlTargetProviderConfig(config) if err != nil { sLog.Errorf(" P (Kubectl Target): expected KubectlTargetProviderConfig - %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to convert to KubectlTargetProviderConfig", providerName), v1alpha2.InitFailed) return err } i.Config = updateConfig var kConfig *rest.Config - if i.Config.InCluster { - kConfig, err = rest.InClusterConfig() - } else { - switch i.Config.ConfigType { - case "path": - if i.Config.ConfigData == "" { - if home := homedir.HomeDir(); home != "" { - i.Config.ConfigData = filepath.Join(home, ".kube", "config") - } else { - err = v1alpha2.NewCOAError(nil, "can't locate home direction to read default kubernetes config file, to run in cluster, set inCluster config setting to true", v1alpha2.BadConfig) - sLog.Errorf(" P (Kubectl Target): %+v", err) - return err - } - } - kConfig, err = clientcmd.BuildConfigFromFlags("", i.Config.ConfigData) - case "inline": - if i.Config.ConfigData != "" { - kConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData)) - if err != nil { - sLog.Errorf(" P (Kubectl Target): failed to get RESTconfg: %+v", err) - return err - } - } else { - err = v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig) - sLog.Errorf(" P (Kubectl Target): %+v", err) - return err - } - default: - err = v1alpha2.NewCOAError(nil, "unrecognized config type, accepted values are: path and inline", v1alpha2.BadConfig) - sLog.Errorf(" P (Kubectl Target): %+v", err) - return err - } - } + kConfig, err = i.getKubernetesConfig() if err != nil { sLog.Errorf(" P (Kubectl Target): failed to get the cluster config: %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get kubernetes config", providerName), v1alpha2.InitFailed) return err } i.Client, err = kubernetes.NewForConfig(kConfig) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to create a new clientset: %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to create kubernetes client", providerName), v1alpha2.InitFailed) return err } i.DynamicClient, err = dynamic.NewForConfig(kConfig) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to create a dynamic client: %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to create dynamic client", providerName), v1alpha2.InitFailed) return err } i.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(kConfig) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to create a discovery client: %+v", err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to create discovery client", providerName), v1alpha2.InitFailed) + return err + } + + i.MetaPopulator, err = metahelper.NewMetaPopulator(metahelper.WithDefaultPopulators()) + if err != nil { + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to create meta populator", providerName), v1alpha2.InitFailed) + sLog.Error(err) return err } i.Mapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(i.DiscoveryClient)) i.RESTConfig = kConfig + + if providerOperationMetrics == nil { + providerOperationMetrics, err = metrics.New() + if err != nil { + sLog.Error(err) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to init metrics", providerName), v1alpha2.InitFailed) + return err + } + } + return nil } +func (i *KubectlTargetProvider) getKubernetesConfig() (*rest.Config, error) { + if i.Config.InCluster { + return rest.InClusterConfig() + } + + switch i.Config.ConfigType { + case "path": + return i.getConfigFromPath() + case "inline": + return i.getConfigFromInline() + default: + err := v1alpha2.NewCOAError(nil, "unrecognized config type, accepted values are: path and inline", v1alpha2.BadConfig) + sLog.Errorf(" P (Kubectl Target): failed to get the cluster config: %+v", err) + return nil, err + } +} + +func (i *KubectlTargetProvider) getConfigFromPath() (*rest.Config, error) { + if i.Config.ConfigData == "" { + home := homedir.HomeDir() + if home == "" { + err := v1alpha2.NewCOAError(nil, "can't locate home directory to read default kubernetes config file. To run in cluster, set inCluster to true", v1alpha2.BadConfig) + sLog.Errorf(" P (Kubectl Target): %+v", err) + return nil, err + } + i.Config.ConfigData = filepath.Join(home, ".kube", "config") + } + return clientcmd.BuildConfigFromFlags("", i.Config.ConfigData) +} + +func (i *KubectlTargetProvider) getConfigFromInline() (*rest.Config, error) { + if i.Config.ConfigData == "" { + err := v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig) + sLog.Errorf(" P (Kubectl Target): %+v", err) + return nil, err + } + return clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData)) +} + // toKubectlTargetProviderConfig converts a generic IProviderConfig to a KubectlTargetProviderConfig func toKubectlTargetProviderConfig(config providers.IProviderConfig) (KubectlTargetProviderConfig, error) { ret := KubectlTargetProviderConfig{} @@ -225,18 +283,19 @@ func (i *KubectlTargetProvider) Get(ctx context.Context, deployment model.Deploy select { case dataBytes, ok := <-chanMes: if !ok { - err = errors.New("failed to receive from data channel") sLog.Errorf(" P (Kubectl Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: failed to receive from data channel when reading yaml property", providerName), v1alpha2.GetComponentSpecFailed) return nil, err } _, err = i.getCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope) if err != nil { if kerrors.IsNotFound(err) { - sLog.Infof(" P (Kubectl Target): resource not found: %s, traceId: %s", err, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Kubectl Target): custom resource not found: %s, traceId: %s", err, span.SpanContext().TraceID().String()) continue } sLog.Errorf(" P (Kubectl Target): failed to read object: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get custom resource from data bytes in yaml property", providerName), v1alpha2.GetComponentSpecFailed) return nil, err } @@ -245,7 +304,7 @@ func (i *KubectlTargetProvider) Get(ctx context.Context, deployment model.Deploy case err, ok := <-chanErr: if !ok { - err = errors.New("failed to receive from error channel") + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: failed to receive from err channel when reading yaml property", providerName), v1alpha2.GetComponentSpecFailed) sLog.Errorf(" P (Kubectl Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) return nil, err } @@ -254,6 +313,7 @@ func (i *KubectlTargetProvider) Get(ctx context.Context, deployment model.Deploy stop = true } else { sLog.Errorf(" P (Kubectl Target): failed to apply Yaml: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to read yaml property", providerName), v1alpha2.GetComponentSpecFailed) return nil, err } } @@ -263,23 +323,25 @@ func (i *KubectlTargetProvider) Get(ctx context.Context, deployment model.Deploy dataBytes, err = json.Marshal(component.Component.Properties["resource"]) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to get deployment bytes from component: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get data bytes from component resource property", providerName), v1alpha2.GetComponentSpecFailed) return nil, err } _, err = i.getCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope) if err != nil { if kerrors.IsNotFound(err) { - sLog.Infof(" P (Kubectl Target): resource not found: %v, traceId: %s", err, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Kubectl Target): custom resource not found: %v, traceId: %s", err, span.SpanContext().TraceID().String()) continue } sLog.Errorf(" P (Kubectl Target): failed to read object: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get custom resource from data bytes in component resource property", providerName), v1alpha2.GetComponentSpecFailed) return nil, err } ret = append(ret, component.Component) } else { - err = errors.New("component doesn't have yaml or resource property") + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: component doesn't have yaml or resource property", providerName), v1alpha2.GetComponentSpecFailed) sLog.Errorf(" P (Kubectl Target): component doesn't have yaml or resource property, traceId: %s", span.SpanContext().TraceID().String()) return nil, err } @@ -301,10 +363,21 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl defer utils.CloseSpanWithError(span, &err) sLog.Infof(" P (Kubectl Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + functionName := utils.GetFunctionName() + applyTime := time.Now().UTC() components := step.GetComponents() err = i.GetValidationRule(ctx).Validate(components) if err != nil { + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ValidateRuleOperation, + metrics.CreateOperationType, + v1alpha2.ValidateFailed.String(), + ) + sLog.Errorf(" P (Kubectl Target): failed to validate components, error: %v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: the rule validation failed", providerName), v1alpha2.ValidateFailed) return nil, err } if isDryRun { @@ -315,6 +388,7 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl components = step.GetUpdatedComponents() if len(components) > 0 { for _, component := range components { + applyComponentTime := time.Now().UTC() if component.Type == "yaml.k8s" { if v, ok := component.Properties["yaml"].(string); ok { chanMes, chanErr := readYaml(v) @@ -323,22 +397,63 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl select { case dataBytes, ok := <-chanMes: if !ok { - err = errors.New("failed to receive from data channel") + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: failed to receive from data channel when reading yaml property", providerName), v1alpha2.ReadYamlFailed) sLog.Errorf(" P (Kubectl Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ReceiveDataChannelOperation, + metrics.GetOperationType, + v1alpha2.ReadYamlFailed.String(), + ) return ret, err } i.ensureNamespace(ctx, deployment.Instance.Spec.Scope) - err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope) + err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope, *deployment.Instance.Spec) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to apply Yaml: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to apply Yaml", providerName), v1alpha2.ApplyYamlFailed) + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ApplyYamlOperation, + metrics.UpdateOperationType, + v1alpha2.ApplyYamlFailed.String(), + ) + return ret, err } + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.Updated, + Message: fmt.Sprintf("No error. %s has been updated", component.Name), + } + case err, ok := <-chanErr: if !ok { - err = errors.New("failed to receive from error channel") + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: failed to receive from error channel when reading yaml property", providerName), v1alpha2.ReadYamlFailed) sLog.Errorf(" P (Kubectl Target): %+v, traceId: %s, traceId: %s", err, span.SpanContext().TraceID().String()) + + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ReceiveErrorChannelOperation, + metrics.UpdateOperationType, + v1alpha2.ReadYamlFailed.String(), + ) return ret, err } @@ -346,6 +461,19 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl stop = true } else { sLog.Errorf(" P (Kubectl Target): failed to apply Yaml: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to apply Yaml", providerName), v1alpha2.ApplyYamlFailed) + + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ApplyYamlOperation, + metrics.UpdateOperationType, + v1alpha2.ApplyYamlFailed.String(), + ) return ret, err } } @@ -355,27 +483,103 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl dataBytes, err = json.Marshal(component.Properties["resource"]) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to convert resource data to bytes: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to convert resource data to bytes", providerName), v1alpha2.ReadResourcePropertyFailed) + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ConvertResourceDataBytesOperation, + metrics.UpdateOperationType, + v1alpha2.ReadResourcePropertyFailed.String(), + ) + return ret, err } i.ensureNamespace(ctx, deployment.Instance.Spec.Scope) - err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope) + err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope, *deployment.Instance.Spec) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to apply custom resource: %+v, traceId: %s", err, err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to apply custom resource", providerName), v1alpha2.ApplyResourceFailed) + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ApplyCustomResource, + metrics.UpdateOperationType, + v1alpha2.ApplyResourceFailed.String(), + ) + return ret, err } + // check the resource status + if component.Properties["statusProbe"] != nil { + //check the status propbe property + statusProbe, err := toStatusProbe(component.Properties["statusProbe"]) + if err != nil { + sLog.Errorf("Status property is not correctly defined: +%v", err) + } + resourceStatus, err := i.checkResourceStatus(ctx, dataBytes, deployment.Instance.Spec.Scope, statusProbe, component.Name) + if err != nil { + sLog.Errorf("Failed to check resource status: +%v", err) + } + ret[component.Name] = resourceStatus + } else { + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.Updated, + Message: fmt.Sprintf("No error. %s has been updated", component.Name), + } + } + } else { - err = errors.New("component doesn't have yaml property or resource property") + err := v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: component doesn't have yaml or resource property", providerName), v1alpha2.YamlResourcePropertyNotFound) sLog.Errorf(" P (Kubectl Target): component doesn't have yaml property or resource property, traceId: %s", err, span.SpanContext().TraceID().String()) + + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ApplyOperation, + metrics.UpdateOperationType, + v1alpha2.YamlResourcePropertyNotFound.String(), + ) return ret, err } } + + providerOperationMetrics.ProviderOperationLatency( + applyComponentTime, + kubectl, + functionName, + metrics.ApplyOperation, + metrics.UpdateOperationType, + ) } } + + providerOperationMetrics.ProviderOperationLatency( + applyTime, + kubectl, + functionName, + metrics.ApplyOperation, + metrics.UpdateOperationType, + ) + + deleteTime := time.Now().UTC() components = step.GetDeletedComponents() if len(components) > 0 { for _, component := range components { + deleteComponentTime := time.Now().UTC() if component.Type == "yaml.k8s" { if v, ok := component.Properties["yaml"].(string); ok { chanMes, chanErr := readYaml(v) @@ -384,21 +588,64 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl select { case dataBytes, ok := <-chanMes: if !ok { - err = errors.New("failed to receive from data channel") + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: failed to receive from data channel when reading yaml property", providerName), v1alpha2.ReadYamlFailed) sLog.Errorf(" P (Kubectl Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.DeleteFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ReceiveDataChannelOperation, + metrics.DeleteOperationType, + v1alpha2.ReadYamlFailed.String(), + ) return ret, err } err = i.deleteCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope) - if err != nil { + if err != nil && !kerrors.IsNotFound(err) { sLog.Errorf(" P (Kubectl Target): failed to read object: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to delete object from yaml property", providerName), v1alpha2.DeleteYamlFailed) + + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.DeleteFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ObjectOperation, + metrics.DeleteOperationType, + v1alpha2.DeleteYamlFailed.String(), + ) + return ret, err } + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.Deleted, + Message: "", + } + case err, ok := <-chanErr: if !ok { - err = errors.New("failed to receive from error channel") + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to receive from err channel when reading yaml property", providerName), v1alpha2.ReadYamlFailed) sLog.Errorf(" P (Kubectl Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.DeleteFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ReceiveErrorChannelOperation, + metrics.DeleteOperationType, + v1alpha2.ReadYamlFailed.String(), + ) return ret, err } @@ -406,6 +653,19 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl stop = true } else { sLog.Errorf(" P (Kubectl Target): failed to remove resource: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to delete object from yaml property", providerName), v1alpha2.DeleteYamlFailed) + + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.DeleteFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ResourceOperation, + metrics.DeleteOperationType, + v1alpha2.DeleteYamlFailed.String(), + ) return ret, err } } @@ -415,26 +675,205 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl dataBytes, err = json.Marshal(component.Properties["resource"]) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to convert resource data to bytes: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to convert resource data to bytes", providerName), v1alpha2.ReadResourcePropertyFailed) + + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.DeleteFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ConvertResourceDataBytesOperation, + metrics.DeleteOperationType, + v1alpha2.ReadResourcePropertyFailed.String(), + ) return ret, err } err = i.deleteCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope) - if err != nil { + if err != nil && !kerrors.IsNotFound(err) { sLog.Errorf(" P (Kubectl Target): failed to delete custom resource: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to delete custom resource", providerName), v1alpha2.DeleteResourceFailed) + + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.DeleteFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ApplyCustomResource, + metrics.DeleteOperationType, + v1alpha2.DeleteResourceFailed.String(), + ) return ret, err } + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.Deleted, + Message: "", + } + } else { - err = errors.New("component doesn't have yaml property or resource property") + err = v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: component doesn't have yaml or resource property", providerName), v1alpha2.DeleteFailed) sLog.Errorf(" P (Kubectl Target): component doesn't have yaml property or resource property, traceId: %s", span.SpanContext().TraceID().String()) + ret[component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.DeleteFailed, + Message: err.Error(), + } + providerOperationMetrics.ProviderOperationErrors( + kubectl, + functionName, + metrics.ApplyOperation, + metrics.DeleteOperationType, + v1alpha2.YamlResourcePropertyNotFound.String(), + ) return ret, err } } + + providerOperationMetrics.ProviderOperationLatency( + deleteComponentTime, + kubectl, + functionName, + metrics.ApplyOperation, + metrics.DeleteOperationType, + ) } } + + providerOperationMetrics.ProviderOperationLatency( + deleteTime, + kubectl, + functionName, + metrics.ApplyOperation, + metrics.DeleteOperationType, + ) + providerOperationMetrics.ProviderOperationLatency( + applyTime, + kubectl, + functionName, + metrics.ApplyOperation, + metrics.CreateOperationType, + ) + return ret, nil } +// checkResourceStatus checks the status of the resource +func (k *KubectlTargetProvider) checkResourceStatus(ctx context.Context, dataBytes []byte, namespace string, status *StatusProbe, componentName string) (model.ComponentResultSpec, error) { + _, span := observability.StartSpan( + "Kubectl Target Provider", + ctx, + &map[string]string{ + "method": "checkResourceStatus", + }, + ) + var err error + result := model.ComponentResultSpec{} + defer utils.CloseSpanWithError(span, &err) + + //add initial wait before checking the resource status + if status.InitialWait == "" { + status.InitialWait = initialWait + } + + waitTime, _ := time.ParseDuration(status.InitialWait) + time.Sleep(waitTime) + if status.Timeout == "" { + status.Timeout = timeout + } + + timeout, _ := time.ParseDuration(status.Timeout) + if status.Interval == "" { + status.Interval = interval + } + + interval, _ := time.ParseDuration(status.Interval) + + // set default values for succeeded and failed values + if status.SucceededValues == nil { + status.SucceededValues = []string{"Succeeded"} + } + + if status.FailedValues == nil { + status.FailedValues = []string{"Failed"} + } + + if namespace == "" { + namespace = constants.DefaultScope + } + + resource, err := k.getCustomResource(ctx, dataBytes, namespace) + if err != nil { + return result, err + } + + context, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + compiledPath, err := jsonpath.Compile(status.StatusPath) + if err != nil { + sLog.Error("Failed to parse the the status path: +%v", err) + } + + errorPath, err := jsonpath.Compile(status.ErrorMessagePath) + if err != nil { + sLog.Error("Failed to parse the error message path: +%v", err) + } + + for { + select { + case <-context.Done(): + err := v1alpha2.NewCOAError(nil, fmt.Sprintf("%s: failed to get the status of the resource within the timeout period", providerName), v1alpha2.CheckResourceStatusFailed) + result = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: err.Error(), + } + return result, err + case <-time.After(interval): + // checks if the status of the resource is the same as the status in the CRD status probe property + resourceStatus, err := compiledPath.Lookup(resource.Object) + sLog.Infof("Checking the resource status: %v", resourceStatus) + if err != nil { + // if the path is not found then continue to wait for the status + sLog.Errorf("Warning - waiting for reosurce to be created: +%v", err) + } + + if resourceStatus != nil { + // check for succeeded values + for _, succeededValue := range status.SucceededValues { + sLog.Infof("Checking the resource status for succeededValue: %v", succeededValue) + if resourceStatus.(string) == succeededValue { + result = model.ComponentResultSpec{ + Status: v1alpha2.Updated, + Message: fmt.Sprintf("No error. %s has been updated", componentName), + } + return result, nil + } + } + // check for failed values + for _, failedValue := range status.FailedValues { + sLog.Infof("Checking the resource status for failedValue: %v", failedValue) + if resourceStatus.(string) == failedValue { + // get the error message from the resource + errorMessage, err := errorPath.Lookup(resource.Object) + if err != nil { + errorMessage = "failed to apply custom resource" + } + + result = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: fmt.Sprintf("%s: %s", providerName, errorMessage.(string)), + } + return result, nil + } + } + } + } + } +} + // ensureNamespace ensures that the namespace exists func (k *KubectlTargetProvider) ensureNamespace(ctx context.Context, namespace string) error { _, span := observability.StartSpan( @@ -463,7 +902,7 @@ func (k *KubectlTargetProvider) ensureNamespace(ctx context.Context, namespace s Name: namespace, }, }, metav1.CreateOptions{}) - if err != nil { + if err != nil && !kerrors.IsAlreadyExists(err) { sLog.Errorf(" P (Kubectl Target): failed to create namespace: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) return err } @@ -487,9 +926,8 @@ func (*KubectlTargetProvider) GetValidationRule(ctx context.Context) model.Valid RequiredMetadata: []string{}, OptionalMetadata: []string{}, ChangeDetectionProperties: []model.PropertyDesc{ - { - Name: "*", //react to all property changes - }, + {Name: "yaml", IgnoreCase: false, SkipIfMissing: true}, + {Name: "resource", IgnoreCase: false, SkipIfMissing: true}, }, }, } @@ -600,7 +1038,7 @@ func (i *KubectlTargetProvider) deleteCustomResource(ctx context.Context, dataBy } // applyCustomResource applies a custom resource from a byte array -func (i *KubectlTargetProvider) applyCustomResource(ctx context.Context, dataBytes []byte, namespace string) error { +func (i *KubectlTargetProvider) applyCustomResource(ctx context.Context, dataBytes []byte, namespace string, instance model.InstanceSpec) error { obj, dr, err := i.buildDynamicResourceClient(dataBytes, namespace) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to build a new dynamic client: %+v", err) @@ -614,6 +1052,12 @@ func (i *KubectlTargetProvider) applyCustomResource(ctx context.Context, dataByt sLog.Errorf(" P (Kubectl Target): failed to read object: %+v", err) return err } + + if err = i.MetaPopulator.PopulateMeta(obj, instance); err != nil { + sLog.Errorf(" P (Kubectl Target): failed to populate meta: +%v", err) + return err + } + // Create the object _, err = dr.Create(ctx, obj, metav1.CreateOptions{}) if err != nil { @@ -623,6 +1067,10 @@ func (i *KubectlTargetProvider) applyCustomResource(ctx context.Context, dataByt return nil } + if err = i.MetaPopulator.PopulateMeta(obj, instance); err != nil { + sLog.Errorf(" P (Kubectl Target): failed to populate meta: +%v", err) + return err + } // Update the object obj.SetResourceVersion(existing.GetResourceVersion()) _, err = dr.Update(ctx, obj, metav1.UpdateOptions{}) @@ -633,3 +1081,23 @@ func (i *KubectlTargetProvider) applyCustomResource(ctx context.Context, dataByt return nil } + +// toStatusProbe converts a component status property to a status probe property +func toStatusProbe(status interface{}) (*StatusProbe, error) { + statusProbe, ok := status.(map[string]interface{}) + if !ok { + return nil, errors.New("statusProbe property is not present in the component") + } + ret := StatusProbe{} + data, err := json.Marshal(statusProbe) + if err != nil { + return nil, err + } + + err = json.Unmarshal(data, &ret) + if err != nil { + return nil, err + } + + return &ret, nil +} diff --git a/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl_test.go b/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl_test.go index 24fc30433..400fa9c8e 100644 --- a/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl_test.go @@ -145,8 +145,8 @@ func TestReadYamlFromUrl(t *testing.T) { // TestKubectlTargetProviderApply tests that applying a deployment works func TestKubectlTargetProviderPathApplyAndDelete(t *testing.T) { - testGatekeeper := os.Getenv("TEST_KUBECTL") - if testGatekeeper == "" { + testKubectl := os.Getenv("TEST_KUBECTL") + if testKubectl == "" { t.Skip("Skipping because TEST_KUBECTL environment variable is not set") } config := KubectlTargetProviderConfig{ @@ -158,17 +158,19 @@ func TestKubectlTargetProviderPathApplyAndDelete(t *testing.T) { err := provider.Init(config) assert.Nil(t, err) component := model.ComponentSpec{ - Name: "gatekeeper", + Name: "nginx", Type: "yaml.k8s", Properties: map[string]interface{}{ - "yaml": "https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml", + // use nginx deployment as an example to replace gatekeeper tests since insufficient cleanup for gatekeeper deployment may break other cases. + "yaml": "https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/application/deployment.yaml", + // "yaml": "https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml", }, } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ Spec: &model.InstanceSpec{ - Scope: "gatekeeper-system", - Name: "gatekeeper", + Scope: "nginx-deployment", + Name: "nginx", }, }, Solution: model.SolutionState{ @@ -199,6 +201,9 @@ func TestKubectlTargetProviderPathApplyAndDelete(t *testing.T) { } _, err = provider.Apply(context.Background(), deployment, step, false) assert.Nil(t, err) + + // sleep 30s and wait for sufficient cleanup for gatekeeper + time.Sleep(30 * time.Second) } func TestKubectlTargetProviderInlineApply(t *testing.T) { @@ -278,6 +283,206 @@ func TestKubectlTargetProviderInlineApply(t *testing.T) { _, err = provider.Apply(context.Background(), deployment, step, false) assert.Nil(t, err) } + +// TestKubectlTargetProviderInlineUpdate tests that updating a component works +func TestKubectlTargetProviderInlineUpdate(t *testing.T) { + testGatekeeper := os.Getenv("TEST_KUBECTL") + if testGatekeeper == "" { + t.Skip("Skipping because TEST_KUBECTL environment variable is not set") + } + // wait for 5 sec before updating the deployment + time.Sleep(5 * time.Second) + + config := KubectlTargetProviderConfig{ + InCluster: false, + ConfigType: "path"} + provider := KubectlTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + component := model.ComponentSpec{ + Name: "nginx", + Type: "yaml.k8s", + Properties: map[string]interface{}{ + "resource": map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "nginx", + }, + "spec": map[string]interface{}{ + "replicas": 4, + "selector": map[string]interface{}{ + "matchLabels": map[string]string{ + "app": "nginx", + }, + }, + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]string{ + "app": "nginx", + }, + }, + "spec": map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "name": "nginx", + "image": "nginx:1.17.0", + "ports": []map[string]interface{}{ + { + "containerPort": 80, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + deployment := model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{ + Name: "test-instance-iu", + Scope: "test-scope-iu", + }, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + } + // update + step := model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) + + time.Sleep(3 * time.Second) + + // cleanup + step = model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentDelete, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) +} + +// TestKubectlTargetProviderInlineStatusProbeApply tests that applying a deployment with a status probe works +func TestKubectlTargetProviderInlineStatusProbeApply(t *testing.T) { + testGatekeeper := os.Getenv("TEST_KUBECTL") + if testGatekeeper == "" { + t.Skip("Skipping because TEST_KUBECTL environment variable is not set") + } + + config := KubectlTargetProviderConfig{ + InCluster: false, + ConfigType: "path"} + provider := KubectlTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + component := model.ComponentSpec{ + Name: "nginxtest", + Type: "yaml.k8s", + Properties: map[string]interface{}{ + "resource": map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "nginxtest", + }, + "spec": map[string]interface{}{ + "replicas": 3, + "selector": map[string]interface{}{ + "matchLabels": map[string]string{ + "app": "nginxtest", + }, + }, + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]string{ + "app": "nginxtest", + }, + }, + "spec": map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "name": "nginx", + "image": "nginx:1.16.1", + "ports": []map[string]interface{}{ + { + "containerPort": 80, + }, + }, + }, + }, + }, + }, + }, + }, + "statusProbe": map[string]interface{}{ + "succeededValues": []string{"True"}, + "failedValues": []string{"Failed"}, + "statusPath": "$.status.conditions[0].status", + "errorMessagePath": "$.status.conditions[0].message", + "timeout": "1m", + "interval": "2s", + "initialWait": "10s", + }, + }, + } + deployment := model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{ + Name: "test-instance-spa", + Scope: "test-scope-spa", + }, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + } + // update + step := model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) + + time.Sleep(3 * time.Second) + + // cleanup + step = model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentDelete, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) +} + func TestKubectlTargetProviderClusterLevelInlineApply(t *testing.T) { testGatekeeper := os.Getenv("TEST_KUBECTL") if testGatekeeper == "" { diff --git a/api/pkg/apis/v1alpha1/providers/target/metrics/attributes.go b/api/pkg/apis/v1alpha1/providers/target/metrics/attributes.go new file mode 100644 index 000000000..eaa931497 --- /dev/null +++ b/api/pkg/apis/v1alpha1/providers/target/metrics/attributes.go @@ -0,0 +1,24 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package metrics + +// Target gets common logging attributes for a target. +func Target( + providerType string, + functionName string, + operation string, + operationType string, + errorCode string, +) map[string]any { + return map[string]any{ + "providerType": providerType, + "functionName": functionName, + "operation": operation, + "operationType": operationType, + "errorCode": errorCode, + } +} diff --git a/api/pkg/apis/v1alpha1/providers/target/metrics/metrics.go b/api/pkg/apis/v1alpha1/providers/target/metrics/metrics.go new file mode 100644 index 000000000..160539690 --- /dev/null +++ b/api/pkg/apis/v1alpha1/providers/target/metrics/metrics.go @@ -0,0 +1,131 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package metrics + +import ( + "time" + + "github.com/eclipse-symphony/symphony/api/constants" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" +) + +const ( + ValidateRuleOperation string = "ValidateRule" + ApplyScriptOperation string = "ApplyScript" + ApplyOperation string = "Apply" + ApplyYamlOperation string = "ApplyYaml" + ApplyCustomResource string = "ApplyCustomResource" + ReceiveDataChannelOperation string = "ReceiveFromDataChannel" + ReceiveErrorChannelOperation string = "ReceiveFromErrorChannel" + ConvertResourceDataBytesOperation string = "ConvertResourceDataToBytes" + ObjectOperation string = "Object" + ResourceOperation string = "Resource" + PullChartOperation string = "PullChart" + LoadChartOperation string = "LoadChart" + HelmChartOperation string = "HelmChart" + HelmActionConfigOperation string = "HelmActionConfig" + HelmPropertiesOperation string = "HelmProperties" + + GetOperationType string = "Get" + CreateOperationType string = "Create" + UpdateOperationType string = "Update" + DeleteOperationType string = "Delete" +) + +// Metrics is a metrics tracker for a provider operation. +type Metrics struct { + providerOperationLatency observability.Histogram + providerOperationErrors observability.Counter +} + +func New() (*Metrics, error) { + observable := observability.New(constants.API) + + providerOperationLatency, err := observable.Metrics.Histogram( + "symphony_provider_operation_latency", + "measure of overall latency for provider operation side", + ) + if err != nil { + return nil, err + } + + providerOperationErrors, err := observable.Metrics.Counter( + "symphony_provider_operation_errors", + "count of errors in provider operation side", + ) + if err != nil { + return nil, err + } + + return &Metrics{ + providerOperationLatency: providerOperationLatency, + providerOperationErrors: providerOperationErrors, + }, nil +} + +// Close closes all metrics. +func (m *Metrics) Close() { + if m == nil { + return + } + + m.providerOperationErrors.Close() +} + +// ProviderOperationLatency tracks the overall provider target latency. +func (m *Metrics) ProviderOperationLatency( + startTime time.Time, + providerType string, + operation string, + operationType string, + functionName string, +) { + if m == nil { + return + } + + m.providerOperationLatency.Add( + latency(startTime), + Target( + providerType, + functionName, + operation, + operationType, + v1alpha2.OK.String(), + ), + ) +} + +// ProviderOperationErrors increments the count of errors for provider target. +func (m *Metrics) ProviderOperationErrors( + providerType string, + functionName string, + operation string, + operationType string, + errorCode string, +) { + if m == nil { + return + } + + m.providerOperationErrors.Add( + 1, + Target( + providerType, + functionName, + operation, + operationType, + errorCode, + ), + ) +} + +// Latency gets the time since the given start in milliseconds. +func latency(start time.Time) float64 { + return float64(time.Since(start)) / float64(time.Millisecond) +} diff --git a/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt.go b/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt.go index b39ad7b89..2b97485d7 100644 --- a/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt.go +++ b/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt.go @@ -25,7 +25,9 @@ import ( "github.com/google/uuid" ) -var sLog = logger.NewLogger("coa.runtime") +const loggerName = "providers.target.mqtt" + +var sLog = logger.NewLogger(loggerName) type MQTTTargetProviderConfig struct { Name string `json:"name"` diff --git a/api/pkg/apis/v1alpha1/providers/target/proxy/proxy.go b/api/pkg/apis/v1alpha1/providers/target/proxy/proxy.go index 65faeceff..c138b84bc 100644 --- a/api/pkg/apis/v1alpha1/providers/target/proxy/proxy.go +++ b/api/pkg/apis/v1alpha1/providers/target/proxy/proxy.go @@ -25,7 +25,9 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/logger" ) -var sLog = logger.NewLogger("coa.runtime") +const loggerName = "providers.target.proxy" + +var sLog = logger.NewLogger(loggerName) type ProxyUpdateProviderConfig struct { Name string `json:"name"` diff --git a/api/pkg/apis/v1alpha1/providers/target/script/script.go b/api/pkg/apis/v1alpha1/providers/target/script/script.go index 135966565..57a6e0d77 100644 --- a/api/pkg/apis/v1alpha1/providers/target/script/script.go +++ b/api/pkg/apis/v1alpha1/providers/target/script/script.go @@ -11,15 +11,16 @@ import ( "encoding/json" "errors" "io" - "io/ioutil" "net/http" "net/url" "os" "os/exec" "path/filepath" "strings" + "time" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/metrics" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" @@ -29,7 +30,15 @@ import ( "github.com/google/uuid" ) -var sLog = logger.NewLogger("coa.runtime") +const ( + script = "script" + loggerName = "providers.target.script" +) + +var ( + sLog = logger.NewLogger(loggerName) + providerOperationMetrics *metrics.Metrics +) type ScriptProviderConfig struct { Name string `json:"name"` @@ -129,8 +138,18 @@ func (i *ScriptProvider) Init(config providers.IProviderConfig) error { } } + if providerOperationMetrics == nil { + providerOperationMetrics, err = metrics.New() + if err != nil { + return err + } + } + + observ_utils.CloseSpanWithError(span, nil) + return nil } + func downloadFile(scriptFolder string, script string, stagingFolder string) error { sPath, err := url.JoinPath(scriptFolder, script) if err != nil { @@ -181,11 +200,11 @@ func (i *ScriptProvider) Get(ctx context.Context, deployment model.DeploymentSpe staging := filepath.Join(i.Config.StagingFolder, input) file, _ := json.MarshalIndent(deployment, "", " ") - _ = ioutil.WriteFile(staging, file, 0644) + _ = os.WriteFile(staging, file, 0644) staging_ref := filepath.Join(i.Config.StagingFolder, input_ref) file_ref, _ := json.MarshalIndent(references, "", " ") - _ = ioutil.WriteFile(staging_ref, file_ref, 0644) + _ = os.WriteFile(staging_ref, file_ref, 0644) abs, _ := filepath.Abs(staging) abs_ref, _ := filepath.Abs(staging_ref) @@ -208,7 +227,7 @@ func (i *ScriptProvider) Get(ctx context.Context, deployment model.DeploymentSpe outputStaging := filepath.Join(i.Config.StagingFolder, output) - data, err := ioutil.ReadFile(outputStaging) + data, err := os.ReadFile(outputStaging) if err != nil { sLog.Errorf(" P (Script Target): failed to read output file: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) @@ -235,11 +254,11 @@ func (i *ScriptProvider) runScriptOnComponents(deployment model.DeploymentSpec, stagingDeployment := filepath.Join(i.Config.StagingFolder, deploymentId) file, _ := json.MarshalIndent(deployment, "", " ") - _ = ioutil.WriteFile(stagingDeployment, file, 0644) + _ = os.WriteFile(stagingDeployment, file, 0644) stagingRef := filepath.Join(i.Config.StagingFolder, currenRefId) file, _ = json.MarshalIndent(components, "", " ") - _ = ioutil.WriteFile(stagingRef, file, 0644) + _ = os.WriteFile(stagingRef, file, 0644) absDeployment, _ := filepath.Abs(stagingDeployment) absRef, _ := filepath.Abs(stagingRef) @@ -269,10 +288,10 @@ func (i *ScriptProvider) runScriptOnComponents(deployment model.DeploymentSpec, outputStaging := filepath.Join(i.Config.StagingFolder, output) - data, err := ioutil.ReadFile(outputStaging) + data, err := os.ReadFile(outputStaging) if err != nil { - sLog.Errorf(" P (Script Target): failed to pread output file: %+v", err) + sLog.Errorf(" P (Script Target): failed to parse apply script output (expected map[string]model.ComponentResultSpec): %+v", err) return nil, err } @@ -296,8 +315,16 @@ func (i *ScriptProvider) Apply(ctx context.Context, deployment model.DeploymentS defer observ_utils.CloseSpanWithError(span, &err) sLog.Infof(" P (Script Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + functionName := observ_utils.GetFunctionName() err = i.GetValidationRule(ctx).Validate([]model.ComponentSpec{}) //this provider doesn't handle any components TODO: is this right? if err != nil { + providerOperationMetrics.ProviderOperationErrors( + script, + functionName, + metrics.ValidateRuleOperation, + metrics.CreateOperationType, + v1alpha2.ValidateFailed.String(), + ) return nil, err } if isDryRun { @@ -305,6 +332,7 @@ func (i *ScriptProvider) Apply(ctx context.Context, deployment model.DeploymentS return nil, nil } + applyTime := time.Now().UTC() ret := step.PrepareResultMap() components := step.GetUpdatedComponents() if len(components) > 0 { @@ -312,24 +340,61 @@ func (i *ScriptProvider) Apply(ctx context.Context, deployment model.DeploymentS retU, err = i.runScriptOnComponents(deployment, components, false) if err != nil { sLog.Errorf(" P (Script Target): failed to run apply script: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + providerOperationMetrics.ProviderOperationErrors( + script, + functionName, + metrics.ApplyScriptOperation, + metrics.UpdateOperationType, + v1alpha2.ApplyScriptFailed.String(), + ) return nil, err } for k, v := range retU { ret[k] = v } } + providerOperationMetrics.ProviderOperationLatency( + applyTime, + script, + functionName, + metrics.ApplyScriptOperation, + metrics.UpdateOperationType, + ) + + deleteTime := time.Now().UTC() components = step.GetDeletedComponents() if len(components) > 0 { var retU map[string]model.ComponentResultSpec retU, err = i.runScriptOnComponents(deployment, components, true) if err != nil { sLog.Errorf(" P (Script Target): failed to run remove script: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + providerOperationMetrics.ProviderOperationErrors( + script, + functionName, + metrics.ApplyScriptOperation, + metrics.DeleteOperationType, + v1alpha2.RemoveScriptFailed.String(), + ) return nil, err } for k, v := range retU { ret[k] = v } } + providerOperationMetrics.ProviderOperationLatency( + deleteTime, + script, + functionName, + metrics.ApplyScriptOperation, + metrics.DeleteOperationType, + ) + providerOperationMetrics.ProviderOperationLatency( + applyTime, + script, + functionName, + metrics.ApplyOperation, + metrics.CreateOperationType, + ) return ret, nil } func (*ScriptProvider) GetValidationRule(ctx context.Context) model.ValidationRule { diff --git a/api/pkg/apis/v1alpha1/providers/target/staging/staging.go b/api/pkg/apis/v1alpha1/providers/target/staging/staging.go index f3245b534..94dbcf16e 100644 --- a/api/pkg/apis/v1alpha1/providers/target/staging/staging.go +++ b/api/pkg/apis/v1alpha1/providers/target/staging/staging.go @@ -20,7 +20,9 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/logger" ) -var sLog = logger.NewLogger("coa.runtime") +const loggerName = "providers.target.staging" + +var sLog = logger.NewLogger(loggerName) type StagingTargetProviderConfig struct { Name string `json:"name"` diff --git a/api/pkg/apis/v1alpha1/utils/apiclient.go b/api/pkg/apis/v1alpha1/utils/apiclient.go new file mode 100644 index 000000000..440dfa7f2 --- /dev/null +++ b/api/pkg/apis/v1alpha1/utils/apiclient.go @@ -0,0 +1,614 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package utils + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + + "github.com/eclipse-symphony/symphony/api/constants" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/fsnotify/fsnotify" + + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" +) + +type ( + apiClient struct { + baseUrl string + tokenProvider TokenProvider + client *http.Client + caCertPath string + } + + ApiClientOption func(*apiClient) + + TokenProvider func(ctx context.Context, baseUrl string, client *http.Client) (string, error) + + SummaryGetter interface { + GetSummary(ctx context.Context, id string, namespace string) (*model.SummaryResult, error) + } + + Dispatcher interface { + QueueJob(ctx context.Context, id string, namespace string, isDelete bool, isTarget bool) error + QueueDeploymentJob(ctx context.Context, namespace string, isDelete bool, deployment model.DeploymentSpec) error + } + + ApiClient interface { + SummaryGetter + Dispatcher + GetInstancesForAllNamespaces(ctx context.Context) ([]model.InstanceState, error) + GetInstances(ctx context.Context, namespace string) ([]model.InstanceState, error) + GetInstance(ctx context.Context, instance string, namespace string) (model.InstanceState, error) + CreateInstance(ctx context.Context, instance string, payload []byte, namespace string) error + DeleteInstance(ctx context.Context, instance string, namespace string) error + DeleteTarget(ctx context.Context, target string, namespace string) error + GetSolutions(ctx context.Context, namespace string) ([]model.SolutionState, error) + GetSolution(ctx context.Context, solution string, namespace string) (model.SolutionState, error) + CreateSolution(ctx context.Context, solution string, payload []byte, namespace string) error + DeleteSolution(ctx context.Context, solution string, namespace string) error + GetTargetsForAllNamespaces(ctx context.Context) ([]model.TargetState, error) + GetTarget(ctx context.Context, target string, namespace string) (model.TargetState, error) + GetTargets(ctx context.Context, namespace string) ([]model.TargetState, error) + CreateTarget(ctx context.Context, target string, payload []byte, namespace string) error + Reconcile(ctx context.Context, deployment model.DeploymentSpec, isDelete bool, namespace string) (model.SummarySpec, error) + } +) + +func noTokenProvider(ctx context.Context, baseUrl string, client *http.Client) (string, error) { + return "", nil +} + +func WithUserPassword(ctx context.Context, user string, password string) ApiClientOption { + return func(a *apiClient) { + a.tokenProvider = func(ctx context.Context, baseUrl string, _ *http.Client) (string, error) { + request := authRequest{Username: user, Password: password} + requestData, _ := json.Marshal(request) + ret, err := a.callRestAPI(ctx, "users/auth", "POST", requestData, "") + if err != nil { + return "", err + } + + var response authResponse + err = json.Unmarshal(ret, &response) + if err != nil { + return "", err + } + + return response.AccessToken, nil + } + } +} + +func WithServiceAccountToken() ApiClientOption { + return func(a *apiClient) { + a.tokenProvider = func(ctx context.Context, _ string, _ *http.Client) (string, error) { + path := os.Getenv(constants.SATokenPathName) + if path == "" { + path = constants.SATokenPath + } + token, err := os.ReadFile(path) + if err != nil { + return "", v1alpha2.NewCOAError(nil, "Token creation error: unable to read from volume.", v1alpha2.InternalError) + } + return string(token), nil + } + } +} + +func WithCertAuth(caCertPath string) ApiClientOption { + return func(a *apiClient) { + a.caCertPath = caCertPath + } +} + +func NewAPIClient(ctx context.Context, baseUrl string, opts ...ApiClientOption) (*apiClient, error) { + rUrl, err := url.Parse(baseUrl) + if err != nil { + return nil, err + } + + isSecure := rUrl.Scheme == "https" + + client, err := newHttpClient(ctx, isSecure) + if err != nil { + return nil, err + } + + a := &apiClient{ + baseUrl: baseUrl, + tokenProvider: noTokenProvider, + client: client, + } + + for _, opt := range opts { + opt(a) + } + + return a, nil +} + +func (a *apiClient) GetInstances(ctx context.Context, namespace string) ([]model.InstanceState, error) { + ret := make([]model.InstanceState, 0) + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return ret, err + } + response, err := a.callRestAPI(ctx, "instances?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) GetInstancesForAllNamespaces(ctx context.Context) ([]model.InstanceState, error) { + ret := make([]model.InstanceState, 0) + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "instances", "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) GetInstance(ctx context.Context, instance string, namespace string) (model.InstanceState, error) { + ret := model.InstanceState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "instances/"+url.QueryEscape(instance)+"?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) CreateInstance(ctx context.Context, instance string, payload []byte, namespace string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return err + } + //use proper url encoding in the following statement + _, err = a.callRestAPI(ctx, "instances/"+url.QueryEscape(instance)+"?namespace="+url.QueryEscape(namespace), "POST", payload, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) DeleteInstance(ctx context.Context, instance string, namespace string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "instances/"+url.QueryEscape(instance)+"?direct=true&namespace="+url.QueryEscape(namespace), "DELETE", nil, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) DeleteTarget(ctx context.Context, target string, namespace string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "targets/registry/"+url.QueryEscape(target)+"?direct=true&namespace="+url.QueryEscape(namespace), "DELETE", nil, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) GetSolutions(ctx context.Context, namespace string) ([]model.SolutionState, error) { + ret := make([]model.SolutionState, 0) + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "solutions?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) GetSolution(ctx context.Context, solution string, namespace string) (model.SolutionState, error) { + ret := model.SolutionState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "solutions/"+url.QueryEscape(solution)+"?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) CreateSolution(ctx context.Context, solution string, payload []byte, namespace string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "solutions/"+url.QueryEscape(solution)+"?namespace="+url.QueryEscape(namespace), "POST", payload, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) DeleteSolution(ctx context.Context, solution string, namespace string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "solutions/"+url.QueryEscape(solution)+"?namespace="+url.QueryEscape(namespace), "DELETE", nil, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) GetTarget(ctx context.Context, target string, namespace string) (model.TargetState, error) { + ret := model.TargetState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "targets/registry/"+url.QueryEscape(target)+"?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) GetTargets(ctx context.Context, namespace string) ([]model.TargetState, error) { + ret := []model.TargetState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "targets/registry?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) GetTargetsForAllNamespaces(ctx context.Context) ([]model.TargetState, error) { + ret := []model.TargetState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "targets/registry", "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) CreateTarget(ctx context.Context, target string, payload []byte, namespace string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "targets/registry/"+url.QueryEscape(target)+"?namespace="+url.QueryEscape(namespace), "POST", payload, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) GetSummary(ctx context.Context, id string, namespace string) (*model.SummaryResult, error) { + result := model.SummaryResult{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return nil, err + } + + log.Debugf("apiClient.GetSummary: id: %s, namespace: %s", id, namespace) + ret, err := a.callRestAPI(ctx, "solution/queue?instance="+url.QueryEscape(id)+"&namespace="+url.QueryEscape(namespace), "GET", nil, token) + // callRestApi Does a weird thing where it returns nil if the status code is 404 so we'll recreate the error here + if err == nil && ret == nil { + log.Debugf("apiClient.GetSummary: Not found") + return nil, v1alpha2.NewCOAError(nil, "Not found", v1alpha2.NotFound) + } + + if err != nil { + return nil, err + } + if ret != nil { + log.Debugf("apiClient.GetSummary: ret: %s", string(ret)) + err = json.Unmarshal(ret, &result) + if err != nil { + return nil, err + } + } + return &result, nil +} + +func (a *apiClient) QueueDeploymentJob(ctx context.Context, namespace string, isDelete bool, deployment model.DeploymentSpec) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + path := "solution/queue" + query := url.Values{ + "namespace": []string{namespace}, + "delete": []string{fmt.Sprintf("%t", isDelete)}, + "objectType": []string{"deployment"}, + } + var payload []byte + if err != nil { + return err + } + payload, err = json.Marshal(deployment) + log.Debugf("apiClient.QueueDeploymentJob: Deployment payload: %s", string(payload)) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, fmt.Sprintf("%s?%s", path, query.Encode()), "POST", payload, token) + if err != nil { + return err + } + return nil +} + +// Deprecated: Use QueueDeploymentJob instead +func (a *apiClient) QueueJob(ctx context.Context, id string, namespace string, isDelete bool, isTarget bool) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return err + } + path := "solution/queue" + query := url.Values{ + "instance": []string{id}, + "namespace": []string{namespace}, + "delete": []string{fmt.Sprintf("%t", isDelete)}, + "objectType": []string{"instance"}, + } + + if isTarget { + query.Set("objectType", "target") + } + + _, err = a.callRestAPI(ctx, fmt.Sprintf("%s?%s", path, query.Encode()), "POST", nil, token) // TODO: We can pass empty token now because is path is a "back-door", as it was designed to be invoked from a trusted environment, which should be also protected with auth + if err != nil { + return err + } + return nil +} + +func (a *apiClient) Reconcile(ctx context.Context, deployment model.DeploymentSpec, isDelete bool, namespace string) (model.SummarySpec, error) { + summary := model.SummarySpec{} + payload, _ := json.Marshal(deployment) + + path := "solution/reconcile" + "?namespace=" + namespace + if isDelete { + path = path + "&delete=true" + } + token, err := a.tokenProvider(ctx, a.baseUrl, a.client) + if err != nil { + return summary, err + } + ret, err := a.callRestAPI(ctx, path, "POST", payload, token) // TODO: We can pass empty token now because is path is a "back-door", as it was designed to be invoked from a trusted environment, which should be also protected with auth + if err != nil { + return summary, err + } + if ret != nil { + err = json.Unmarshal(ret, &summary) + if err != nil { + return summary, err + } + } + return summary, nil +} + +func (a *apiClient) callRestAPI(ctx context.Context, route string, method string, payload []byte, token string) ([]byte, error) { + urlString := fmt.Sprintf("%s%s", a.baseUrl, path.Clean(route)) + ctx, span := observability.StartSpan("Symphony-API-Client", ctx, &map[string]string{ + "method": "callRestAPI", + "http.method": method, + "http.url": urlString, + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + var rUrl *url.URL + rUrl, err = url.Parse(urlString) + if err != nil { + return nil, err + } + var req *http.Request + var reqBody io.Reader + if payload != nil { + reqBody = bytes.NewBuffer(payload) + } + + req, err = http.NewRequestWithContext(ctx, method, rUrl.String(), reqBody) + observ_utils.PropagateSpanContextToHttpRequestHeader(req) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + var resp *http.Response + resp, err = a.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var bodyBytes []byte + bodyBytes, err = io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode >= 300 { + if resp.StatusCode == 404 { // API service is already gone + return nil, nil + } + object := &SummarySpecError{ + Code: fmt.Sprintf("Symphony API: [%d]", resp.StatusCode), + Message: string(bodyBytes), + } + return nil, object + } + + return bodyBytes, nil +} + +func newHttpClient(ctx context.Context, secure bool) (*http.Client, error) { + client := &http.Client{} + if !secure { + return client, nil + } + + certBytes, err := os.ReadFile(apiCertPath) + if err != nil { + return nil, err + } + + updateTransport := func(certBytes []byte) { + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(certBytes) + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: caCertPool, + InsecureSkipVerify: false, + }, + } + } + + updateTransport(certBytes) + + // setup a file watcher to reload the cert pool when the symphony cert changes + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + // watch for cert changes + go func() { + defer watcher.Close() + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Has(fsnotify.Write) { + newCertBytes, readErr := os.ReadFile(apiCertPath) + if readErr != nil { + continue + } + updateTransport(newCertBytes) + } + case _, ok := <-watcher.Errors: + if !ok { + return + } + case <-ctx.Done(): + return + } + } + }() + + err = watcher.Add(apiCertPath) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/api/pkg/apis/v1alpha1/utils/metahelper/metahelper.go b/api/pkg/apis/v1alpha1/utils/metahelper/metahelper.go new file mode 100644 index 000000000..a7124f7bc --- /dev/null +++ b/api/pkg/apis/v1alpha1/utils/metahelper/metahelper.go @@ -0,0 +1,90 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package metahelper + +import ( + "github.com/eclipse-symphony/symphony/api/constants" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ( + MetaPopulator interface { + PopulateMeta(object metaV1.Object, instance model.InstanceSpec) error + } + metaPopulator struct { + annotationPopulators []func(instance model.InstanceSpec) (map[string]string, error) + labelPopulators []func(instance model.InstanceSpec) (map[string]string, error) + } + Option func(*metaPopulator) +) + +func NewMetaPopulator(opts ...Option) (*metaPopulator, error) { + mp := &metaPopulator{} + for _, opt := range opts { + opt(mp) + } + return mp, nil +} + +func WithDefaultPopulators() Option { + return func(mp *metaPopulator) { + mp.annotationPopulators = append(mp.annotationPopulators, populateDefaultAnnotations) + mp.labelPopulators = append(mp.labelPopulators, populateDefaultLabels) + } +} + +func (m *metaPopulator) PopulateMeta(object metaV1.Object, instance model.InstanceSpec) error { + if err := m.populateLabels(object, instance); err != nil { + return err + } + if err := m.populateAnnotations(object, instance); err != nil { + return err + } + return nil +} + +func (m *metaPopulator) populateLabels(object metaV1.Object, instance model.InstanceSpec) error { + var labels []map[string]string + labels = append(labels, object.GetLabels()) + for _, f := range m.labelPopulators { + label, err := f(instance) + if err != nil { + return err + } + labels = append(labels, label) + } + object.SetLabels(utils.MergeCollection(labels...)) + return nil +} + +func (m *metaPopulator) populateAnnotations(object metaV1.Object, instance model.InstanceSpec) error { + var annotations []map[string]string + annotations = append(annotations, object.GetAnnotations()) + for _, f := range m.annotationPopulators { + annotation, err := f(instance) + if err != nil { + return err + } + annotations = append(annotations, annotation) + } + object.SetAnnotations(utils.MergeCollection(annotations...)) + return nil +} + +func populateDefaultLabels(instance model.InstanceSpec) (map[string]string, error) { + labels := make(map[string]string) + labels[constants.ManagerMetaKey] = constants.API + return labels, nil +} + +func populateDefaultAnnotations(instance model.InstanceSpec) (map[string]string, error) { + annotations := make(map[string]string) + annotations[constants.InstanceMetaKey] = instance.Name + return annotations, nil +} diff --git a/api/pkg/apis/v1alpha1/utils/symphony-api.go b/api/pkg/apis/v1alpha1/utils/symphony-api.go index d19eceb4f..c5cee2c29 100644 --- a/api/pkg/apis/v1alpha1/utils/symphony-api.go +++ b/api/pkg/apis/v1alpha1/utils/symphony-api.go @@ -14,8 +14,10 @@ import ( "io/ioutil" "net/http" "net/url" + "os" "strings" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" @@ -24,8 +26,11 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/logger" ) -const ( +var ( SymphonyAPIAddressBase = "http://symphony-service:8080/v1alpha2/" + symphonyAPIAddressBase = os.Getenv(constants.SymphonyAPIUrlEnvName) + useSAToken = os.Getenv(constants.UseServiceAccountTokenEnvName) + apiCertPath = os.Getenv(constants.ApiCertEnvName) ) type authRequest struct { @@ -42,18 +47,29 @@ type authResponse struct { // We shouldn't use specific error types // SummarySpecError represents an error that includes a SummarySpec in its message // field. -// type SummarySpecError struct { -// Code string `json:"code"` -// Message string `json:"message"` -// } - -// func (e *SummarySpecError) Error() string { -// return fmt.Sprintf( -// "failed to invoke Symphony API: [%s] - %s", -// e.Code, -// e.Message, -// ) -// } +type SummarySpecError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +func (e *SummarySpecError) Error() string { + return fmt.Sprintf( + "failed to invoke Symphony API: [%s] - %s", + e.Code, + e.Message, + ) +} + +func GetSymphonyAPIAddressBase() string { + if symphonyAPIAddressBase == "" { + return SymphonyAPIAddressBase + } + return symphonyAPIAddressBase +} + +func ShouldUseSATokens() bool { + return useSAToken == "true" +} var log = logger.NewLogger("coa.runtime") @@ -594,11 +610,16 @@ func MatchTargets(instance model.InstanceState, targets []model.TargetState) []m return slice } -func CreateSymphonyDeploymentFromTarget(target model.TargetState) (model.DeploymentSpec, error) { +func CreateSymphonyDeploymentFromTarget(target model.TargetState, namespace string) (model.DeploymentSpec, error) { key := fmt.Sprintf("%s-%s", "target-runtime", target.ObjectMeta.Name) scope := target.Spec.Scope + if scope == "" { + scope = constants.DefaultScope + } - ret := model.DeploymentSpec{} + ret := model.DeploymentSpec{ + ObjectNamespace: namespace, + } solution := model.SolutionState{ ObjectMeta: model.ObjectMeta{ Name: key, @@ -644,6 +665,8 @@ func CreateSymphonyDeploymentFromTarget(target model.TargetState) (model.Deploym ret.Instance = instance ret.Targets = targets ret.SolutionName = key + // set the target generation to the deployment + ret.Generation = target.Spec.Generation assignments, err := AssignComponentsToTargets(ret.Solution.Spec.Components, ret.Targets) if err != nil { return ret, err @@ -657,8 +680,10 @@ func CreateSymphonyDeploymentFromTarget(target model.TargetState) (model.Deploym return ret, nil } -func CreateSymphonyDeployment(instance model.InstanceState, solution model.SolutionState, targets []model.TargetState, devices []model.DeviceState) (model.DeploymentSpec, error) { - ret := model.DeploymentSpec{} +func CreateSymphonyDeployment(instance model.InstanceState, solution model.SolutionState, targets []model.TargetState, devices []model.DeviceState, namespace string) (model.DeploymentSpec, error) { + ret := model.DeploymentSpec{ + ObjectNamespace: namespace, + } ret.Generation = instance.Spec.Generation // convert targets @@ -667,6 +692,10 @@ func CreateSymphonyDeployment(instance model.InstanceState, solution model.Solut sTargets[t.ObjectMeta.Name] = t } + if instance.Spec.Scope == "" { + instance.Spec.Scope = constants.DefaultScope + } + //TODO: handle devices ret.Solution = solution ret.Targets = sTargets @@ -701,7 +730,9 @@ func AssignComponentsToTargets(components []model.ComponentSpec, targets map[str parser := NewParser(component.Constraints) val, err := parser.Eval(utils.EvaluationContext{Properties: target.Spec.Properties}) if err != nil { - return ret, err + // append the error message with the component constraint expression + errMsg := fmt.Sprintf("%s in constraint expression: %s", err.Error(), component.Constraints) + return ret, v1alpha2.NewCOAError(nil, errMsg, v1alpha2.TargetPropertyNotFound) } match = (val == "true" || val == true) } @@ -731,6 +762,9 @@ func GetSummary(context context.Context, baseUrl string, user string, password s return result, err } } + + log.Infof("Summary result: %s", string(ret)) + return result, nil } func CatalogHook(context context.Context, baseUrl string, user string, password string, payload []byte) error { diff --git a/api/pkg/apis/v1alpha1/utils/symphony-api_test.go b/api/pkg/apis/v1alpha1/utils/symphony-api_test.go index 755715660..ed01411aa 100644 --- a/api/pkg/apis/v1alpha1/utils/symphony-api_test.go +++ b/api/pkg/apis/v1alpha1/utils/symphony-api_test.go @@ -23,6 +23,12 @@ const ( password = "" ) +var ( + testApiClient ApiClient = &apiClient{ + baseUrl: baseUrl, + } +) + func TestGetInstancesWhenSomeInstances(t *testing.T) { testSymphonyApi := os.Getenv("TEST_SYMPHONY_API") if testSymphonyApi != "yes" { @@ -46,7 +52,7 @@ func TestGetInstancesWhenSomeInstances(t *testing.T) { panic(err) } - err = UpsertSolution(context.Background(), baseUrl, solutionName, user, password, solution1, "default") + err = testApiClient.CreateSolution(context.Background(), solutionName, solution1, "default") require.NoError(t, err) targetName := "target1" @@ -56,6 +62,23 @@ func TestGetInstancesWhenSomeInstances(t *testing.T) { "displayName": "int-virtual-02", "scope": "alice-springs", "components": []interface{}{ + map[string]interface{}{ + "name": "observability", + "type": "helm.v3", + "properties": map[string]interface{}{ + "chart": map[string]interface{}{ + "repo": "symphonycr.azurecr.io/sample-dashboard", + "version": "0.4.0-dev", + }, + "values": map[string]interface{}{ + "obsConfig": map[string]interface{}{ + "bluefin": true, + "e4i": true, + "e4k": true, + }, + }, + }, + }, map[string]interface{}{ "name": "e4k", "type": "helm.v3", @@ -134,7 +157,7 @@ func TestGetInstancesWhenSomeInstances(t *testing.T) { target1, err := json.Marshal(target1JsonObj) require.NoError(t, err) - err = CreateTarget(context.Background(), baseUrl, targetName, user, password, target1, "default") + err = testApiClient.CreateTarget(context.Background(), targetName, target1, "default") require.NoError(t, err) instanceName := "instance1" @@ -151,13 +174,13 @@ func TestGetInstancesWhenSomeInstances(t *testing.T) { panic(err) } - err = CreateInstance(context.Background(), baseUrl, instanceName, user, password, instance1, "default") + err = testApiClient.CreateInstance(context.Background(), instanceName, instance1, "default") require.NoError(t, err) // ensure instance gets created properly time.Sleep(time.Second) - instancesRes, err := GetInstances(context.Background(), baseUrl, user, password, "default") + instancesRes, err := testApiClient.GetInstances(context.Background(), "default") require.NoError(t, err) require.Equal(t, 1, len(instancesRes)) @@ -167,7 +190,7 @@ func TestGetInstancesWhenSomeInstances(t *testing.T) { require.Equal(t, "1", instancesRes[0].Status.Properties["targets"]) require.Equal(t, "OK", instancesRes[0].Status.Properties["status"]) - instanceRes, err := GetInstance(context.Background(), baseUrl, instanceName, user, password, "default") + instanceRes, err := testApiClient.GetInstance(context.Background(), instanceName, "default") require.NoError(t, err) require.Equal(t, instanceName, instanceRes.Spec.DisplayName) @@ -176,13 +199,13 @@ func TestGetInstancesWhenSomeInstances(t *testing.T) { require.Equal(t, "1", instanceRes.Status.Properties["targets"]) require.Equal(t, "OK", instanceRes.Status.Properties["status"]) - err = DeleteTarget(context.Background(), baseUrl, targetName, user, password, "default") + err = testApiClient.DeleteTarget(context.Background(), targetName, "default") require.NoError(t, err) - err = DeleteSolution(context.Background(), baseUrl, solutionName, user, password, "default") + err = testApiClient.DeleteSolution(context.Background(), solutionName, "default") require.NoError(t, err) - err = DeleteInstance(context.Background(), baseUrl, instanceName, user, password, "default") + err = testApiClient.DeleteInstance(context.Background(), instanceName, "default") require.NoError(t, err) } @@ -209,21 +232,21 @@ func TestGetSolutionsWhenSomeSolution(t *testing.T) { panic(err) } - err = UpsertSolution(context.Background(), baseUrl, solutionName, user, password, solution1, "default") + err = testApiClient.CreateSolution(context.Background(), solutionName, solution1, "default") require.NoError(t, err) - solutionsRes, err := GetSolutions(context.Background(), baseUrl, user, password, "default") + solutionsRes, err := testApiClient.GetSolutions(context.Background(), "default") require.NoError(t, err) require.Equal(t, 1, len(solutionsRes)) require.Equal(t, solutionName, solutionsRes[0].Spec.DisplayName) - solutionRes, err := GetSolution(context.Background(), baseUrl, solutionName, user, password, "default") + solutionRes, err := testApiClient.GetSolution(context.Background(), solutionName, "default") require.NoError(t, err) require.Equal(t, solutionName, solutionRes.Spec.DisplayName) - err = DeleteSolution(context.Background(), baseUrl, solutionName, user, password, "default") + err = testApiClient.DeleteSolution(context.Background(), solutionName, "default") require.NoError(t, err) } @@ -240,6 +263,23 @@ func TestGetTargetsWithSomeTargets(t *testing.T) { "displayName": "int-virtual-02", "scope": "alice-springs", "components": []interface{}{ + map[string]interface{}{ + "name": "observability", + "type": "helm.v3", + "properties": map[string]interface{}{ + "chart": map[string]interface{}{ + "repo": "symphonycr.azurecr.io/sample-dashboard", + "version": "0.4.0-dev", + }, + "values": map[string]interface{}{ + "obsConfig": map[string]interface{}{ + "bluefin": true, + "e4i": true, + "e4k": true, + }, + }, + }, + }, map[string]interface{}{ "name": "e4k", "type": "helm.v3", @@ -318,13 +358,13 @@ func TestGetTargetsWithSomeTargets(t *testing.T) { target1, err := json.Marshal(target1JsonObj) require.NoError(t, err) - err = CreateTarget(context.Background(), baseUrl, targetName, user, password, target1, "default") + err = testApiClient.CreateTarget(context.Background(), targetName, target1, "default") require.NoError(t, err) // Ensure target gets created properly time.Sleep(time.Second) - targetsRes, err := GetTargets(context.Background(), baseUrl, user, password, "default") + targetsRes, err := testApiClient.GetTargets(context.Background(), "default") require.NoError(t, err) require.Equal(t, 1, len(targetsRes)) @@ -333,7 +373,7 @@ func TestGetTargetsWithSomeTargets(t *testing.T) { require.Equal(t, "1", targetsRes[0].Status.Properties["targets"]) require.Equal(t, "OK", targetsRes[0].Status.Properties["status"]) - targetRes, err := GetTarget(context.Background(), baseUrl, targetName, user, password, "default") + targetRes, err := testApiClient.GetTarget(context.Background(), targetName, "default") require.NoError(t, err) require.Equal(t, targetName, targetRes.Spec.DisplayName) @@ -341,7 +381,7 @@ func TestGetTargetsWithSomeTargets(t *testing.T) { require.Equal(t, "1", targetRes.Status.Properties["targets"]) require.Equal(t, "OK", targetRes.Status.Properties["status"]) - err = DeleteTarget(context.Background(), baseUrl, targetName, user, password, "default") + err = testApiClient.DeleteTarget(context.Background(), targetName, "default") require.NoError(t, err) } @@ -523,7 +563,7 @@ func TestCreateSymphonyDeploymentFromTarget(t *testing.T) { "key2": "value2", }, }, - }) + }, "default") require.NoError(t, err) ret, err := res.DeepEquals(model.DeploymentSpec{ @@ -676,7 +716,7 @@ func TestCreateSymphonyDeployment(t *testing.T) { }, }, }, - }) + }, "default") require.NoError(t, err) jData, _ := json.Marshal(res) @@ -720,6 +760,7 @@ func TestCreateSymphonyDeployment(t *testing.T) { Spec: &model.InstanceSpec{ Name: "someOtherId", Solution: "", + Scope: "default", // CreateSymphonyDeployment will give default if instance.Spec.Scope is empty Target: model.TargetSelector{ Name: "someTargetName", Selector: map[string]string{ diff --git a/api/pkg/apis/v1alpha1/utils/utils.go b/api/pkg/apis/v1alpha1/utils/utils.go index 4cfb65509..32714efae 100644 --- a/api/pkg/apis/v1alpha1/utils/utils.go +++ b/api/pkg/apis/v1alpha1/utils/utils.go @@ -98,6 +98,7 @@ func ReadStringWithOverrides(col1 map[string]string, col2 map[string]string, key val := ReadString(col1, key, defaultVal) return ReadString(col2, key, val) } + func ContainsString(names []string, name string) bool { for _, n := range names { if n == name { @@ -106,14 +107,12 @@ func ContainsString(names []string, name string) bool { } return false } - -func MergeCollection(col1 map[string]string, col2 map[string]string) map[string]string { +func MergeCollection(cols ...map[string]string) map[string]string { ret := make(map[string]string) - for k, v := range col1 { - ret[k] = v - } - for k, v := range col2 { - ret[k] = v + for _, col := range cols { + for k, v := range col { + ret[k] = v + } } return ret } diff --git a/api/pkg/apis/v1alpha1/vendors/agent-vendor.go b/api/pkg/apis/v1alpha1/vendors/agent-vendor.go index b5f949bdf..6c47ca7f2 100644 --- a/api/pkg/apis/v1alpha1/vendors/agent-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/agent-vendor.go @@ -11,6 +11,7 @@ import ( "encoding/json" "strconv" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/reference" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" @@ -134,7 +135,7 @@ func (c *AgentVendor) doGet(ctx context.Context, parameters map[string]string) v defer span.End() log.Infof("V (Agent): doGet with parameters %v, traceId: %s", parameters, span.SpanContext().TraceID().String()) - var namespace = "default" + var namespace = constants.DefaultScope var kind = "" var ref = "" var group = "" @@ -263,7 +264,7 @@ func (c *AgentVendor) doPost(ctx context.Context, parameters map[string]string, log.Infof("V (Agent): doPost with parameters %v, traceId: %s", parameters, span.SpanContext().TraceID().String()) - var namespace = "default" + var namespace = constants.DefaultScope var kind = "" var group = "" var id = "" diff --git a/api/pkg/apis/v1alpha1/vendors/instances-vendor.go b/api/pkg/apis/v1alpha1/vendors/instances-vendor.go index eff675852..8aaec02b1 100644 --- a/api/pkg/apis/v1alpha1/vendors/instances-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/instances-vendor.go @@ -10,6 +10,7 @@ import ( "encoding/json" "strings" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/instances" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" @@ -84,7 +85,7 @@ func (c *InstancesVendor) onInstances(request v1alpha2.COARequest) v1alpha2.COAR id := request.Parameters["__name"] namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } var err error var state interface{} @@ -119,7 +120,10 @@ func (c *InstancesVendor) onInstances(request v1alpha2.COARequest) v1alpha2.COAR case fasthttp.MethodPost: ctx, span := observability.StartSpan("onInstances-POST", pCtx, nil) id := request.Parameters["__name"] - + namespace, exist := request.Parameters["namespace"] + if !exist { + namespace = constants.DefaultScope + } solution := request.Parameters["solution"] target := request.Parameters["target"] target_selector := request.Parameters["target-selector"] @@ -129,7 +133,8 @@ func (c *InstancesVendor) onInstances(request v1alpha2.COARequest) v1alpha2.COAR if solution != "" && (target != "" || target_selector != "") { instance = model.InstanceState{ ObjectMeta: model.ObjectMeta{ - Name: id, + Name: id, + Namespace: namespace, }, Spec: &model.InstanceSpec{ DisplayName: id, @@ -189,6 +194,7 @@ func (c *InstancesVendor) onInstances(request v1alpha2.COARequest) v1alpha2.COAR Body: v1alpha2.JobData{ Id: id, Action: v1alpha2.JobUpdate, + Scope: instance.ObjectMeta.Namespace, }, }) } @@ -201,7 +207,7 @@ func (c *InstancesVendor) onInstances(request v1alpha2.COARequest) v1alpha2.COAR direct := request.Parameters["direct"] namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } if c.Config.Properties["useJobManager"] == "true" && direct != "true" { c.Context.Publish("job", v1alpha2.Event{ @@ -212,6 +218,7 @@ func (c *InstancesVendor) onInstances(request v1alpha2.COARequest) v1alpha2.COAR Body: v1alpha2.JobData{ Id: id, Action: v1alpha2.JobDelete, + Scope: namespace, }, }) return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ diff --git a/api/pkg/apis/v1alpha1/vendors/job-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/job-vendor_test.go index b3d541e22..191f8c75b 100644 --- a/api/pkg/apis/v1alpha1/vendors/job-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/job-vendor_test.go @@ -63,6 +63,9 @@ func TestJobsInit(t *testing.T) { Type: "managers.symphony.jobs", Properties: map[string]string{ "providers.state": "mem-state", + "baseUrl": "http://localhost:8082/v1alpha2/", + "user": "admin", + "password": "", }, Providers: map[string]managers.ProviderConfig{ "mem-state": { diff --git a/api/pkg/apis/v1alpha1/vendors/solution-vendor.go b/api/pkg/apis/v1alpha1/vendors/solution-vendor.go index 18cc63067..8c181d678 100644 --- a/api/pkg/apis/v1alpha1/vendors/solution-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/solution-vendor.go @@ -1,7 +1,7 @@ /* - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * SPDX-License-Identifier: MIT +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT license. +* SPDX-License-Identifier: MIT */ package vendors @@ -9,7 +9,9 @@ package vendors import ( "context" "encoding/json" + "fmt" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/solution" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" @@ -88,7 +90,7 @@ func (c *SolutionVendor) onQueue(request v1alpha2.COARequest) v1alpha2.COARespon namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } switch request.Method { case fasthttp.MethodGet: @@ -130,7 +132,29 @@ func (c *SolutionVendor) onQueue(request v1alpha2.COARequest) v1alpha2.COARespon defer span.End() instance := request.Parameters["instance"] delete := request.Parameters["delete"] + objectType := request.Parameters["objectType"] target := request.Parameters["target"] + + if objectType == "" { // For backward compatibility + objectType = "instance" + } + + if target == "true" { + objectType = "target" + } + + if objectType == "deployment" { + deployment, err := model.ToDeployment(request.Body) + if err != nil { + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.DeserializeError, + ContentType: "application/json", + Body: []byte(fmt.Sprintf(`{"result":"%s"}`, err.Error())), + }) + } + instance = deployment.Instance.Spec.Name + } + if instance == "" { sLog.Infof("V (Solution): onQueue failed - 400 instance parameter is not found, traceId: %s", span.SpanContext().TraceID().String()) return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ @@ -143,18 +167,16 @@ func (c *SolutionVendor) onQueue(request v1alpha2.COARequest) v1alpha2.COARespon if delete == "true" { action = v1alpha2.JobDelete } - objType := "instance" - if target == "true" { - objType = "target" - } c.Vendor.Context.Publish("job", v1alpha2.Event{ Metadata: map[string]string{ - "objectType": objType, + "objectType": objectType, "namespace": namespace, }, Body: v1alpha2.JobData{ Id: instance, + Scope: namespace, Action: action, + Data: request.Body, }, }) return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ @@ -179,7 +201,7 @@ func (c *SolutionVendor) onReconcile(request v1alpha2.COARequest) v1alpha2.COARe sLog.Infof("V (Solution): onReconcile, method: %s, traceId: %s", request.Method, span.SpanContext().TraceID().String()) namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } switch request.Method { case fasthttp.MethodPost: @@ -233,7 +255,7 @@ func (c *SolutionVendor) onApplyDeployment(request v1alpha2.COARequest) v1alpha2 sLog.Infof("V (Solution): onApplyDeployment %s, traceId: %s", request.Method, span.SpanContext().TraceID().String()) namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } targetName := "" if request.Metadata != nil { diff --git a/api/pkg/apis/v1alpha1/vendors/solutions-vendor.go b/api/pkg/apis/v1alpha1/vendors/solutions-vendor.go index 0147519da..304181a09 100644 --- a/api/pkg/apis/v1alpha1/vendors/solutions-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/solutions-vendor.go @@ -9,6 +9,7 @@ package vendors import ( "encoding/json" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/solutions" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" @@ -78,7 +79,7 @@ func (c *SolutionsVendor) onSolutions(request v1alpha2.COARequest) v1alpha2.COAR uLog.Infof("V (Solutions): onSolutions, method: %s, traceId: %s", request.Method, span.SpanContext().TraceID().String()) namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } switch request.Method { case fasthttp.MethodGet: @@ -117,7 +118,6 @@ func (c *SolutionsVendor) onSolutions(request v1alpha2.COARequest) v1alpha2.COAR case fasthttp.MethodPost: ctx, span := observability.StartSpan("onSolutions-POST", pCtx, nil) id := request.Parameters["__name"] - embed_type := request.Parameters["embed-type"] embed_component := request.Parameters["embed-component"] embed_property := request.Parameters["embed-property"] diff --git a/api/pkg/apis/v1alpha1/vendors/targets-vendor.go b/api/pkg/apis/v1alpha1/vendors/targets-vendor.go index ac9f6c84f..1901cfca8 100644 --- a/api/pkg/apis/v1alpha1/vendors/targets-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/targets-vendor.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/targets" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" @@ -117,7 +118,7 @@ func (c *TargetsVendor) onRegistry(request v1alpha2.COARequest) v1alpha2.COAResp tLog.Infof("V (Targets) : onRegistry, method: %s, traceId: %s", request.Method, span.SpanContext().TraceID().String()) namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } switch request.Method { case fasthttp.MethodGet: @@ -228,6 +229,7 @@ func (c *TargetsVendor) onRegistry(request v1alpha2.COARequest) v1alpha2.COAResp Body: v1alpha2.JobData{ Id: id, Action: v1alpha2.JobUpdate, + Scope: namespace, }, }) } @@ -248,6 +250,7 @@ func (c *TargetsVendor) onRegistry(request v1alpha2.COARequest) v1alpha2.COAResp Body: v1alpha2.JobData{ Id: id, Action: v1alpha2.JobDelete, + Scope: namespace, }, }) return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ @@ -342,7 +345,7 @@ func (c *TargetsVendor) onStatus(request v1alpha2.COARequest) v1alpha2.COARespon case fasthttp.MethodPut: namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } var dict map[string]interface{} json.Unmarshal(request.Body, &dict) @@ -412,7 +415,7 @@ func (c *TargetsVendor) onDownload(request v1alpha2.COARequest) v1alpha2.COAResp case fasthttp.MethodGet: namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } state, err := c.TargetsManager.GetState(pCtx, request.Parameters["__name"], namespace) if err != nil { @@ -463,7 +466,7 @@ func (c *TargetsVendor) onHeartBeat(request v1alpha2.COARequest) v1alpha2.COARes case fasthttp.MethodPost: namespace, exist := request.Parameters["namespace"] if !exist { - namespace = "default" + namespace = constants.DefaultScope } _, err := c.TargetsManager.ReportState(pCtx, model.TargetState{ ObjectMeta: model.ObjectMeta{ diff --git a/api/symphony-k8s-proxy-mqtt.json b/api/symphony-k8s-proxy-mqtt.json index 9f50367d3..92e4fbd72 100644 --- a/api/symphony-k8s-proxy-mqtt.json +++ b/api/symphony-k8s-proxy-mqtt.json @@ -26,7 +26,7 @@ "providers.state": "mem-state", "providers.target": "k8s", "providers.config": "mock-config", - "providers.secret": "mock-secret" + "providers.secret": "mock-secret" }, "providers": { "mem-state": { diff --git a/api/symphony-powershell-over-mqtt.json b/api/symphony-powershell-over-mqtt.json index e96dbc98f..50241a497 100644 --- a/api/symphony-powershell-over-mqtt.json +++ b/api/symphony-powershell-over-mqtt.json @@ -26,7 +26,7 @@ "providers.target": "script", "providers.state": "mem-state", "providers.config": "mock-config", - "providers.secret": "mock-secret" + "providers.secret": "mock-secret" }, "providers": { "script": { diff --git a/api/symphony-script-proxy.json b/api/symphony-script-proxy.json index 80098eeab..4ea744c63 100644 --- a/api/symphony-script-proxy.json +++ b/api/symphony-script-proxy.json @@ -26,7 +26,7 @@ "providers.target": "script", "providers.state": "mem-state", "providers.config": "mock-config", - "providers.secret": "mock-secret" + "providers.secret": "mock-secret" }, "providers": { "script": { diff --git a/api/symphony-win-proxy.json b/api/symphony-win-proxy.json index ebc3c2ace..bcde5bd0f 100644 --- a/api/symphony-win-proxy.json +++ b/api/symphony-win-proxy.json @@ -26,7 +26,7 @@ "providers.target": "sideload", "providers.state": "mem-state", "providers.config": "mock-config", - "providers.secret": "mock-secret" + "providers.secret": "mock-secret" }, "providers": { "sideload": { diff --git a/coa/pkg/apis/v1alpha2/bindings/http/metrics.go b/coa/pkg/apis/v1alpha2/bindings/http/metrics.go index fcb4e66a5..5cefddcff 100644 --- a/coa/pkg/apis/v1alpha2/bindings/http/metrics.go +++ b/coa/pkg/apis/v1alpha2/bindings/http/metrics.go @@ -22,7 +22,7 @@ type Metrics struct { func (m Metrics) Metrics(next fasthttp.RequestHandler) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { - startTime := time.Now() + startTime := time.Now().UTC() next(ctx) diff --git a/coa/pkg/apis/v1alpha2/observability/observability.go b/coa/pkg/apis/v1alpha2/observability/observability.go index 5ff22053d..9cda80fa5 100644 --- a/coa/pkg/apis/v1alpha2/observability/observability.go +++ b/coa/pkg/apis/v1alpha2/observability/observability.go @@ -69,11 +69,11 @@ type Observability struct { } // New returns a new instance of the observability. -func New(aioProject string) Observability { +func New(symphonyProject string) Observability { return Observability{ Metrics: &metrics{ provider: otel.GetMeterProvider(), - meter: otel.GetMeterProvider().Meter(aioProject), + meter: otel.GetMeterProvider().Meter(symphonyProject), }, } } diff --git a/k8s/apis/ai/v1/webhook_suite_test.go b/k8s/apis/ai/v1/webhook_suite_test.go index 4f666c21f..4d1d38f20 100644 --- a/k8s/apis/ai/v1/webhook_suite_test.go +++ b/k8s/apis/ai/v1/webhook_suite_test.go @@ -15,7 +15,9 @@ import ( "testing" "time" - . "github.com/onsi/ginkgo" + . "gopls-workspace/testing" + + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" admissionv1beta1 "k8s.io/api/admission/v1beta1" @@ -25,7 +27,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" ) @@ -43,9 +44,7 @@ func TestAPIs(t *testing.T) { t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) - RunSpecsWithDefaultAndCustomReporters(t, - "Webhook Suite", - []Reporter{printer.NewlineReporter{}}) + RunGinkgoSpecs(t, "Webhook Suite") } var _ = BeforeSuite(func() { @@ -119,7 +118,7 @@ var _ = BeforeSuite(func() { return nil }).Should(Succeed()) -}, 60) +}) var _ = AfterSuite(func() { Skip("Skipping tests for now as they are no longer relevant") diff --git a/k8s/apis/fabric/v1/webhook_suite_test.go b/k8s/apis/fabric/v1/webhook_suite_test.go index 856969a40..011ea2d60 100644 --- a/k8s/apis/fabric/v1/webhook_suite_test.go +++ b/k8s/apis/fabric/v1/webhook_suite_test.go @@ -15,7 +15,9 @@ import ( "testing" "time" - . "github.com/onsi/ginkgo" + . "gopls-workspace/testing" + + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" admissionv1beta1 "k8s.io/api/admission/v1beta1" @@ -26,7 +28,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" ) @@ -44,9 +45,7 @@ func TestAPIs(t *testing.T) { t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) - RunSpecsWithDefaultAndCustomReporters(t, - "Webhook Suite", - []Reporter{printer.NewlineReporter{}}) + RunGinkgoSpecs(t, "Webhook Suite") } var _ = BeforeSuite(func() { @@ -122,7 +121,7 @@ var _ = BeforeSuite(func() { return nil }).Should(Succeed()) -}, 60) +}) var _ = AfterSuite(func() { Skip("Skipping tests for now as they are no longer relevant") diff --git a/k8s/apis/solution/v1/webhook_suite_test.go b/k8s/apis/solution/v1/webhook_suite_test.go index 31e573d35..a65399550 100644 --- a/k8s/apis/solution/v1/webhook_suite_test.go +++ b/k8s/apis/solution/v1/webhook_suite_test.go @@ -15,7 +15,9 @@ import ( "testing" "time" - . "github.com/onsi/ginkgo" + . "gopls-workspace/testing" + + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" admissionv1beta1 "k8s.io/api/admission/v1beta1" @@ -25,7 +27,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" ) @@ -43,9 +44,7 @@ func TestAPIs(t *testing.T) { t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) - RunSpecsWithDefaultAndCustomReporters(t, - "Webhook Suite", - []Reporter{printer.NewlineReporter{}}) + RunGinkgoSpecs(t, "Webhook Suite") } var _ = BeforeSuite(func() { @@ -121,7 +120,7 @@ var _ = BeforeSuite(func() { return nil }).Should(Succeed()) -}, 60) +}) var _ = AfterSuite(func() { Skip("Skipping tests for now as they are no longer relevant") diff --git a/k8s/controllers/ai/suite_test.go b/k8s/controllers/ai/suite_test.go index 82dce5a9c..7df865041 100644 --- a/k8s/controllers/ai/suite_test.go +++ b/k8s/controllers/ai/suite_test.go @@ -10,13 +10,15 @@ import ( "path/filepath" "testing" - . "github.com/onsi/ginkgo" + . "gopls-workspace/testing" + + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -35,9 +37,7 @@ func TestAPIs(t *testing.T) { t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) - RunSpecsWithDefaultAndCustomReporters(t, - "Controller Suite", - []Reporter{printer.NewlineReporter{}}) + RunGinkgoSpecs(t, "Controller Suite") } var _ = BeforeSuite(func() { @@ -63,7 +63,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) -}, 60) +}) var _ = AfterSuite(func() { Skip("Skipping tests for now as they are no longer relevant") diff --git a/k8s/controllers/fabric/suite_test.go b/k8s/controllers/fabric/suite_test.go index b8e8181df..eafa30123 100644 --- a/k8s/controllers/fabric/suite_test.go +++ b/k8s/controllers/fabric/suite_test.go @@ -10,13 +10,15 @@ import ( "path/filepath" "testing" - . "github.com/onsi/ginkgo" + . "gopls-workspace/testing" + + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -35,9 +37,7 @@ func TestAPIs(t *testing.T) { t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) - RunSpecsWithDefaultAndCustomReporters(t, - "Controller Suite", - []Reporter{printer.NewlineReporter{}}) + RunGinkgoSpecs(t, "Controller Suite") } var _ = BeforeSuite(func() { @@ -63,7 +63,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) -}, 60) +}) var _ = AfterSuite(func() { Skip("Skipping tests for now as they are no longer relevant") diff --git a/k8s/controllers/fabric/target_controller.go b/k8s/controllers/fabric/target_controller.go index 3abf01d68..a22a9286d 100644 --- a/k8s/controllers/fabric/target_controller.go +++ b/k8s/controllers/fabric/target_controller.go @@ -55,7 +55,7 @@ func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr myFinalizerName := "target.fabric.symphony/finalizer" log := ctrllog.FromContext(ctx) - log.Info("Reconcile Target") + log.Info("Reconcile Target " + req.Name + " in namespace " + req.Namespace) // Get target target := &symphonyv1.Target{} @@ -78,7 +78,11 @@ func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } summary, err := api_utils.GetSummary(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", fmt.Sprintf("target-runtime-%s", target.ObjectMeta.Name), target.ObjectMeta.Namespace) - if err != nil && !v1alpha2.IsNotFound(err) { + if (err != nil && !v1alpha2.IsNotFound(err)) || (err == nil && !summary.IsDeploymentFinished()) { + if err == nil && !summary.IsDeploymentFinished() { + // mock error if deployment is not finished then cause requeue + err = fmt.Errorf("get summary but deployment is not finished yet") + } uErr := r.updateTargetStatusToReconciling(target, err) if uErr != nil { log.Error(uErr, "failed to update target status to reconciling") @@ -150,7 +154,7 @@ func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr break loop case <-ticker: summary, err := api_utils.GetSummary(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", fmt.Sprintf("target-runtime-%s", target.ObjectMeta.Name), target.ObjectMeta.Namespace) - if err == nil && summary.Summary.IsRemoval == true && summary.Summary.AllAssignedDeployed { + if err == nil && summary.Summary.IsRemoval && summary.IsDeploymentFinished() && summary.Summary.AllAssignedDeployed { break loop } if err != nil && !v1alpha2.IsNotFound(err) { diff --git a/k8s/controllers/solution/instance_controller.go b/k8s/controllers/solution/instance_controller.go index aba37a057..63556c91f 100644 --- a/k8s/controllers/solution/instance_controller.go +++ b/k8s/controllers/solution/instance_controller.go @@ -60,7 +60,7 @@ func (r *InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c myFinalizerName := "instance.solution.symphony/finalizer" log := ctrllog.FromContext(ctx) - log.Info("Reconcile Instance") + log.Info("Reconcile Instance " + req.Name + " in namespace " + req.Namespace) // Get instance instance := &symphonyv1.Instance{} @@ -82,7 +82,11 @@ func (r *InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } summary, err := api_utils.GetSummary(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", instance.ObjectMeta.Name, instance.ObjectMeta.Namespace) - if err != nil && !v1alpha2.IsNotFound(err) { + if (err != nil && !v1alpha2.IsNotFound(err)) || (err == nil && !summary.IsDeploymentFinished()) { + if err == nil && !summary.IsDeploymentFinished() { + // mock error if deployment is not finished then cause requeue + err = fmt.Errorf("get summary but deployment is not finished yet") + } uErr := r.updateInstanceStatusToReconciling(instance, err) if uErr != nil { return ctrl.Result{}, uErr @@ -147,7 +151,7 @@ func (r *InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c break loop case <-ticker: summary, err := api_utils.GetSummary(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", instance.ObjectMeta.Name, instance.ObjectMeta.Namespace) - if err == nil && summary.Summary.IsRemoval == true && summary.Summary.SuccessCount == summary.Summary.TargetCount { + if err == nil && summary.Summary.IsRemoval && summary.IsDeploymentFinished() && summary.Summary.SuccessCount == summary.Summary.TargetCount { break loop } if err != nil && v1alpha2.IsNotFound(err) { diff --git a/k8s/controllers/solution/suite_test.go b/k8s/controllers/solution/suite_test.go index 0294bf5ed..04a356d48 100644 --- a/k8s/controllers/solution/suite_test.go +++ b/k8s/controllers/solution/suite_test.go @@ -11,14 +11,16 @@ import ( "path/filepath" "testing" - . "github.com/onsi/ginkgo" + . "gopls-workspace/testing" + + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/yaml" @@ -38,9 +40,7 @@ func TestAPIs(t *testing.T) { t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) - RunSpecsWithDefaultAndCustomReporters(t, - "Controller Suite", - []Reporter{printer.NewlineReporter{}}) + RunGinkgoSpecs(t, "Controller Suite") } func TestUnmarshalSolution(t *testing.T) { @@ -97,7 +97,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) -}, 60) +}) var _ = AfterSuite(func() { Skip("Skipping tests for now as they are no longer relevant") diff --git a/k8s/go.mod b/k8s/go.mod index 5258a3ad0..0ea998466 100644 --- a/k8s/go.mod +++ b/k8s/go.mod @@ -13,33 +13,32 @@ replace github.com/eclipse-symphony/symphony/packages/mage => ../packages/mage require ( github.com/eclipse-symphony/symphony/api v0.0.0 github.com/eclipse-symphony/symphony/coa v0.0.0 - github.com/eclipse-symphony/symphony/k8s v0.0.0-20211006182710-0b9b3b2b0b0a + github.com/eclipse-symphony/symphony/k8s v0.0.0 github.com/eclipse-symphony/symphony/packages/mage v0.0.0-00010101000000-000000000000 github.com/magefile/mage v1.15.0 - github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.19.0 + github.com/onsi/gomega v1.30.0 github.com/princjef/mageutil v1.0.0 github.com/stretchr/testify v1.8.4 - k8s.io/api v0.25.0 - k8s.io/apimachinery v0.25.0 + k8s.io/api v0.25.2 + k8s.io/apimachinery v0.25.2 k8s.io/client-go v0.25.0 sigs.k8s.io/controller-runtime v0.11.0 ) require ( - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/VividCortex/ewma v1.1.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cheggaaa/pb/v3 v3.0.4 // indirect - github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/emicklei/go-restful/v3 v3.10.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.16.3 // indirect @@ -49,8 +48,10 @@ require ( github.com/mattn/go-runewidth v0.0.9 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect + github.com/onsi/ginkgo/v2 v2.13.1 // indirect github.com/openzipkin/zipkin-go v0.4.1 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.50.0 // indirect go.opentelemetry.io/otel v1.16.0 // indirect @@ -67,6 +68,7 @@ require ( go.opentelemetry.io/otel/trace v1.16.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 // indirect + golang.org/x/tools v0.16.1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect google.golang.org/grpc v1.61.1 // indirect @@ -78,7 +80,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/zapr v1.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -87,41 +89,41 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.4.0 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/imdario/mergo v0.3.13 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.12.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.19.1 - golang.org/x/net v0.19.0 // indirect + golang.org/x/net v0.20.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.25.0 // indirect k8s.io/component-base v0.25.0 // indirect - k8s.io/klog/v2 v2.70.1 // indirect - k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect - k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect + k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 -) +) \ No newline at end of file diff --git a/k8s/go.sum b/k8s/go.sum index 14b5bef07..abcf28d3b 100644 --- a/k8s/go.sum +++ b/k8s/go.sum @@ -33,10 +33,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -72,8 +68,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= -github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= +github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -87,18 +83,19 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= @@ -110,13 +107,15 @@ github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -178,6 +177,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -189,8 +190,9 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 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= @@ -241,8 +243,8 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 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= @@ -264,11 +266,18 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU= +github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= +github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -283,43 +292,51 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -429,7 +446,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -449,15 +465,20 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -523,15 +544,20 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -593,6 +619,9 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 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= @@ -686,6 +715,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/VividCortex/ewma.v1 v1.1.1/go.mod h1:TekXuFipeiHWiAlO1+wSS23vTcyFau5u3rxXUSXj710= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -715,6 +746,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/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= helm.sh/helm/v3 v3.10.0 h1:y/MYONZ/bsld9kHwqgBX2uPggnUr5hahpjwt9/jrHlI= @@ -726,23 +758,22 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0= -k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk= +k8s.io/api v0.25.2 h1:v6G8RyFcwf0HR5jQGIAYlvtRNrxMJQG1xJzaSeVnIS8= +k8s.io/api v0.25.2/go.mod h1:qP1Rn4sCVFwx/xIhe+we2cwBLTXNcheRyYXwajonhy0= k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= -k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= -k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= +k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs= +k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA= k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E= k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8= k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= -k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= -k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= @@ -753,4 +784,4 @@ sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h6 sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= \ No newline at end of file diff --git a/k8s/magefile.go b/k8s/magefile.go index 14c981bf6..fd0f1814e 100644 --- a/k8s/magefile.go +++ b/k8s/magefile.go @@ -13,7 +13,7 @@ import ( "os" //mage:import - _ "github.com/eclipse-symphony/symphony/packages/mage" + base "github.com/eclipse-symphony/symphony/packages/mage" "github.com/magefile/mage/mg" "github.com/princjef/mageutil/bintool" "github.com/princjef/mageutil/shellcmd" @@ -54,10 +54,13 @@ func Manifests() error { // Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. func Generate() error { mg.Deps(ensureControllerGen) - return controllerGen.Command("object:headerFile=hack/boilerplate.go.txt paths=./... paths=../api/pkg/apis/v1alpha1/model").Run() + return shellcmd.RunAll( + controllerGen.Command("object:headerFile=hack/boilerplate.go.txt paths=./..."), + controllerGen.Command("object:headerFile=hack/boilerplate.go.txt paths=../api/pkg/apis/v1alpha1/model"), + ) } -// Run tests. +// Run suites and unit tests in k8s. func OperatorTest() error { mg.Deps(ensureEnvTest) assets, err := envTest.Command(fmt.Sprintf("use %s -p path", EnvTestK8sVersion)).Output() @@ -66,7 +69,21 @@ func OperatorTest() error { } os.Setenv("KUBEBUILDER_ASSETS", string(assets)) - return shellcmd.Command("go test ./... -race -v -coverprofile cover.out").Run() + return base.RunUnitTestAndSuiteTest() +} + +// Run unit tests in k8s. +func OperatorUnitTest() error { + mg.Deps(ensureEnvTest) + + assets, err := envTest.Command(fmt.Sprintf("use %s -p path", EnvTestK8sVersion)).Output() + if err != nil { + return err + } + + os.Setenv("KUBEBUILDER_ASSETS", string(assets)) + + return base.UnitTest() } // Build manager binary. diff --git a/k8s/testing/spec_runner.go b/k8s/testing/spec_runner.go new file mode 100644 index 000000000..d23841af5 --- /dev/null +++ b/k8s/testing/spec_runner.go @@ -0,0 +1,39 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package testing + +import ( + "os" + "testing" + + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/ginkgo/v2/types" +) + +// While we still have a mix of Ginkgo Tests and regular Go tests, there are a +// few considerations to keep in mind: +// - Running `go test` will run all tests, including Ginkgo tests. But it will +// count each Ginkgo test as a single test, regardless of how many `It` specs +// are in the test. Also when generating a report, it will include the +// output of the ginkgo test including unicode control characters. This will +// be inalid in the JUnit report. +// - Running `ginkgo` will run both Ginkgo tests and regular Go tests. But it will +// only generate a report for the Ginkgo tests. By default, the unicode control +// characters will not be included in the report. +// +// Because of this, we need to disable unicode control characters for regular Go tests +// so that it's not included in the report. We would do so by checking the environment +// variable `GOUNIT` (which is set by `mage unitTest`) to determine if we should +// remove the unicode control characters +func RunGinkgoSpecs(t *testing.T, description string, args ...interface{}) bool { + if _, ok := os.LookupEnv("GOUNIT"); ok { + args = append(args, types.ReporterConfig{ + NoColor: true, + }) + } + return ginkgo.RunSpecs(t, description, args...) +} diff --git a/k8s/utils/helper.go b/k8s/utils/helper.go index 748c0eed8..26732df87 100644 --- a/k8s/utils/helper.go +++ b/k8s/utils/helper.go @@ -6,7 +6,9 @@ package utils -import "regexp" +import ( + "regexp" +) func IsComponentKey(key string) bool { regex := regexp.MustCompile(`^targets\.[^.]+\.[^.]+`) diff --git a/packages/mage/mage.go b/packages/mage/mage.go index 1cb1ede17..7cf76eb2c 100644 --- a/packages/mage/mage.go +++ b/packages/mage/mage.go @@ -65,24 +65,6 @@ func ensureDocumenter() error { return documenter.Ensure() } -// EnsureAllTools checks to see if a valid version of the needed tools are -// installed, and downloads/installs them if not. -func EnsureAllTools() error { - if err := ensureFormatter(); err != nil { - return err - } - - if err := ensureLinter(); err != nil { - return err - } - - if err := ensureDocumenter(); err != nil { - return err - } - - return nil -} - func ensureGinkgo() error { return ginkgo.Ensure() } @@ -93,7 +75,7 @@ func ensureGoJUnit() error { // EnsureAllTools checks to see if a valid version of the needed tools are // installed, and downloads/installs them if not. -func EnsureAllTools2() error { +func EnsureAllTools() error { mg.Deps(ensureFormatter, ensureLinter, ensureDocumenter, ensureGinkgo, ensureGoJUnit) return nil @@ -167,7 +149,7 @@ func docCfg() (func(), error) { // Test runs the unit tests. func Test() error { - return shellcmd.Command(`go test -race -timeout 35s -cover ./...`).Run() + return shellcmd.Command(`go test -race -timeout 5m -cover -coverprofile=coverage.out ./...`).Run() } // TestRace runs unit tests without the test cache. @@ -175,7 +157,7 @@ func Test() error { func TestRace() error { return shellcmd.RunAll( `go clean -testcache`, - `go test -race -timeout 35s -cover ./...`, + `go test -race -timeout 5m -cover -coverprofile=coverage.out ./...`, ) } @@ -183,10 +165,24 @@ func TestRace() error { func CleanTest() error { return shellcmd.RunAll( `go clean -testcache`, - `go test -race -timeout 35s -cover ./...`, + `go test -race -timeout 5m -coverprofile=coverage.out ./...`, ) } +// Retrieve the test coverage count from coverage.out file. +func PrintCoverage() error { + file := "coverage.out" + + // check if coverage file exists + _, err := os.Stat(file) + if err != nil { + // throw error if coverage file does not exist + return fmt.Errorf("coverage file (%s) does not exist", file) + } + // print test coverage count + return shellExec(fmt.Sprintf(`go tool cover -func=%s | grep total: | grep -Eo '[0-9]+\.[0-9]+'`, file), false) +} + // Cover checks code coverage from unit tests. func Cover(file string) error { return shellcmd.RunAll( @@ -198,7 +194,7 @@ func Cover(file string) error { } // Test runs both unit and suite tests. -func Test2() error { +func RunUnitTestAndSuiteTest() error { mg.SerialDeps(UnitTest, SuiteTest) return nil } @@ -214,7 +210,7 @@ func UnitTest() error { mg.Deps(ensureGoJUnit) bld.WriteString(" 2>&1 | bin/go-junit-report -set-exit-code -iocopy -out junit-unit-tests.xml") } - err := shellExec(bld.String()) + err := shellExec(bld.String(), true) if err != nil { return err } @@ -315,8 +311,10 @@ func DockerBuild() error { } // Run a command with | or other things that do not work in shellcmd -func shellExec(cmd string) error { - fmt.Println(">", cmd) +func shellExec(cmd string, printCmdOrNot bool) error { + if printCmdOrNot { + fmt.Println(">", cmd) + } execCmd := exec.Command("sh", "-c", cmd) execCmd.Stdout = os.Stdout diff --git a/test/integration/scenarios/00.unit/magefile.go b/test/integration/scenarios/00.unit/magefile.go index 96b0fb5fa..17d4a47c2 100644 --- a/test/integration/scenarios/00.unit/magefile.go +++ b/test/integration/scenarios/00.unit/magefile.go @@ -101,6 +101,7 @@ func Verify() error { // Clean up func Cleanup() { shellExec(fmt.Sprintf("kubectl delete deployment nginx -n default")) + localenvCmd(fmt.Sprintf("dumpSymphonyLogsForTest '%s'", TEST_NAME), "") localenvCmd("destroy all", "") } diff --git a/test/integration/scenarios/01.update/magefile.go b/test/integration/scenarios/01.update/magefile.go index 92a5a3813..3f476e513 100644 --- a/test/integration/scenarios/01.update/magefile.go +++ b/test/integration/scenarios/01.update/magefile.go @@ -93,6 +93,7 @@ func Cleanup() { _ = shellcmd.Command("rm -rf ./manifestForTestingOnly/oss").Run() + localenvCmd(fmt.Sprintf("dumpSymphonyLogsForTest '%s'", TEST_NAME), "") localenvCmd("destroy all", "") } diff --git a/test/integration/scenarios/02.basic/magefile.go b/test/integration/scenarios/02.basic/magefile.go index af9a78c12..e45597dd2 100644 --- a/test/integration/scenarios/02.basic/magefile.go +++ b/test/integration/scenarios/02.basic/magefile.go @@ -177,6 +177,7 @@ func CleanUpSymphonyObjects(namespace string) error { // Clean up func Cleanup() { + localenvCmd(fmt.Sprintf("dumpSymphonyLogsForTest '%s'", TEST_NAME), "") localenvCmd("destroy all", "") } diff --git a/test/integration/scenarios/03.basicWithNsDelete/magefile.go b/test/integration/scenarios/03.basicWithNsDelete/magefile.go index 0a91cf4f8..94013c020 100644 --- a/test/integration/scenarios/03.basicWithNsDelete/magefile.go +++ b/test/integration/scenarios/03.basicWithNsDelete/magefile.go @@ -131,6 +131,7 @@ func Verify() error { // Clean up func Cleanup() { + localenvCmd(fmt.Sprintf("dumpSymphonyLogsForTest '%s'", TEST_NAME), "") localenvCmd("destroy all", "") } diff --git a/test/integration/scenarios/03.basicWithNsDelete/verify/manifest_test.go b/test/integration/scenarios/03.basicWithNsDelete/verify/manifest_test.go index 00328ed83..b33a9bd49 100644 --- a/test/integration/scenarios/03.basicWithNsDelete/verify/manifest_test.go +++ b/test/integration/scenarios/03.basicWithNsDelete/verify/manifest_test.go @@ -129,7 +129,7 @@ func TestBasic_InstanceDeletion(t *testing.T) { fmt.Println("Get namespace before deletion: ", len(namespacesBefore.Items)) // Run a mage command to delete instance - execCmd := exec.Command("sh", "-c", "cd ../../../../localenv && mage remove instance instance03") + execCmd := exec.Command("sh", "-c", "cd ../../../../localenv && mage remove instances.solution.symphony instance03") execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr cmdErr := execCmd.Run() diff --git a/test/integration/scenarios/04.workflow/magefile.go b/test/integration/scenarios/04.workflow/magefile.go index 6f18ee06c..7add4985e 100644 --- a/test/integration/scenarios/04.workflow/magefile.go +++ b/test/integration/scenarios/04.workflow/magefile.go @@ -177,6 +177,7 @@ func Verify() error { // Clean up func Cleanup() { + localenvCmd(fmt.Sprintf("dumpSymphonyLogsForTest '%s'", TEST_NAME), "") localenvCmd("destroy all", "") } diff --git a/test/integration/scenarios/05.catalog/magefile.go b/test/integration/scenarios/05.catalog/magefile.go index d9ad8ccb2..8a529802d 100644 --- a/test/integration/scenarios/05.catalog/magefile.go +++ b/test/integration/scenarios/05.catalog/magefile.go @@ -223,6 +223,7 @@ func listCatalogs(namespace string, dynamicClient dynamic.Interface) (error, *un // Clean up func Cleanup() { + localenvCmd(fmt.Sprintf("dumpSymphonyLogsForTest '%s'", TEST_NAME), "") localenvCmd("destroy all", "") } diff --git a/test/localenv/go.mod b/test/localenv/go.mod index 382463898..0eeb5e687 100644 --- a/test/localenv/go.mod +++ b/test/localenv/go.mod @@ -1,4 +1,4 @@ -module dev.azure.com/msazure/One/_git/symphony.git/localenv +module github.com/eclipse-symphony/symphony/test/localenv go 1.20 diff --git a/test/localenv/magefile.go b/test/localenv/magefile.go index 9e9baaf7e..5ec3cd6c6 100644 --- a/test/localenv/magefile.go +++ b/test/localenv/magefile.go @@ -166,6 +166,49 @@ func BuildUp() error { return nil } +// Run a command with | or other things that do not work in shellcmd +func shellExec(cmd string, printCmdOrNot bool) error { + if printCmdOrNot { + fmt.Println(">", cmd) + } + + execCmd := exec.Command("sh", "-c", cmd) + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + + return execCmd.Run() +} + +// Collect Symphony logs to a log folder provided +func Logs(logRootFolder string) error { + // api logs + apiLogFile := fmt.Sprintf("%s/api.log", logRootFolder) + k8sLogFile := fmt.Sprintf("%s/k8s.log", logRootFolder) + + err := shellExec(fmt.Sprintf("kubectl logs 'deployment/symphony-api' --all-containers -n %s > %s", NAMESPACE, apiLogFile), true) + + if err != nil { + return err + } + + err = shellExec(fmt.Sprintf("kubectl logs 'deployment/symphony-controller-manager' --all-containers -n %s > %s", NAMESPACE, k8sLogFile), true) + + return err +} + +// Dump symphony api and k8s logs for tests +func DumpSymphonyLogsForTest(testName string) { + normalizedTestName := strings.Replace(testName, "/", "_", -1) + normalizedTestName = strings.Replace(normalizedTestName, " ", "_", -1) + + logFolderName := fmt.Sprintf("test_%s_%s", normalizedTestName, time.Now().Format("20060102150405")) + logRootFolder := fmt.Sprintf("/tmp/symhony-integration-test-logs/%s", logFolderName) + + _ = shellcmd.Command(fmt.Sprintf("mkdir -p %s", logRootFolder)).Run() + + _ = Logs(logRootFolder) +} + // Uninstall all components, e.g. mage destroy all func Destroy(flags string) error { err := shellcmd.RunAll( @@ -215,6 +258,27 @@ func (Build) All() error { return nil } +// Store the docker images to tar files +func (Build) Save() error { + defer logTime(time.Now(), "build:save") + + err := saveDockerImageToTarFile("symphony-k8s:latest.tar", "ghcr.io/eclipse-symphony/symphony-k8s:latest") + if err != nil { + return err + } + + err = saveDockerImageToTarFile("symphony-api:latest.tar", "ghcr.io/eclipse-symphony/symphony-api:latest") + if err != nil { + return err + } + + return nil +} + +func saveDockerImageToTarFile(tarFilePath string, imageTag string) error { + return shellcmd.Command(fmt.Sprintf("docker image save -o %s %s", tarFilePath, imageTag)).Run() +} + // Build api container func (Build) Api() error { return buildAPI() @@ -431,7 +495,7 @@ func GhcrLogin() error { // Remove Symphony resource func Remove(resourceType, resourceName string) error { - fmt.Println("Deleting resource %s %s", resourceType, resourceName) + fmt.Printf("Deleting resource %s %s\n", resourceType, resourceName) err := shellcmd.RunAll(shellcmd.Command(fmt.Sprintf("kubectl delete %s %s", resourceType, resourceName))) if err != nil { return err @@ -531,7 +595,7 @@ func waitForServiceCleanup() error { return fmt.Errorf("Failed to clean up all the resources!") } - o, err := shellcmd.Command.Output(`kubectl get pods -A --no-headers`) + o, err := shellcmd.Command.Output(`kubectl get pods -A --output=jsonpath='{range .items[*]}{@.metadata.namespace}{"|"}{@.metadata.name}{"\n"}{end}'`) if err != nil { return err } @@ -540,13 +604,11 @@ func waitForServiceCleanup() error { notReady := make([]string, 0) for _, pod := range pods { - if len(strings.TrimSpace(pod)) > 3 && !strings.Contains(pod, "kube-system") { - parts := strings.Split(pod, " ") - name := pod - if len(parts) >= 2 { - name = parts[1] - } - notReady = append(notReady, name) + parts := strings.Split(pod, "|") + pod = parts[1] + namespace := parts[0] + if namespace != "kube-system" { + notReady = append(notReady, pod) } } From c492ff7b4f274793b993857335e0797926151cd4 Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Thu, 2 May 2024 13:48:40 -0700 Subject: [PATCH 06/26] opera target view --- docs/samples/opera/Dockerfile | 37 + docs/samples/opera/app/api/solutions/route.ts | 5 +- docs/samples/opera/app/targets/page.tsx | 40 + docs/samples/opera/app/types.d.ts | 79 +- docs/samples/opera/components/Filter.tsx | 39 + docs/samples/opera/components/MultiView.tsx | 13 +- docs/samples/opera/components/SideBar.tsx | 2 +- .../opera/components/TargetSpecCard.tsx | 104 + .../opera/components/assets/AssetCard.tsx | 6 +- .../opera/components/assets/AssetList.tsx | 39 +- .../opera/components/catalogs/CatalogCard.tsx | 4 +- .../components/editors/CatalogEditor.tsx | 38 +- .../opera/components/graph/GraphTable.tsx | 16 +- .../components/solutions/SolutionCard.tsx | 4 +- .../opera/components/targets/TargetCard.tsx | 73 + .../components/targets/TargetCardList.tsx | 32 + docs/samples/opera/package-lock.json | 4675 ++++++++++------- docs/samples/opera/package.json | 3 +- 18 files changed, 3375 insertions(+), 1834 deletions(-) create mode 100644 docs/samples/opera/Dockerfile create mode 100644 docs/samples/opera/app/targets/page.tsx create mode 100644 docs/samples/opera/components/Filter.tsx create mode 100644 docs/samples/opera/components/TargetSpecCard.tsx create mode 100644 docs/samples/opera/components/targets/TargetCard.tsx create mode 100644 docs/samples/opera/components/targets/TargetCardList.tsx diff --git a/docs/samples/opera/Dockerfile b/docs/samples/opera/Dockerfile new file mode 100644 index 000000000..7d3932aa9 --- /dev/null +++ b/docs/samples/opera/Dockerfile @@ -0,0 +1,37 @@ +# Step 1: Build the base image with node.js +FROM node:16-alpine as builder + +# Set the working directory in the container +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json package-lock.json ./ + +# Install dependencies +RUN npm ci +# Copy the rest of the application code +COPY . . + +# Build the application +RUN npm run build + +# Step 2: Use a smaller base image for the production environment +FROM node:16-alpine + +# Set the working directory in the container +WORKDIR /app + +# Install the Next.js production server +RUN npm install next + +# Copy the build artifacts from the builder stage +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json + +# Expose the port Next.js runs on +EXPOSE 3000 + +# Command to run the application +CMD ["npm", "start"] diff --git a/docs/samples/opera/app/api/solutions/route.ts b/docs/samples/opera/app/api/solutions/route.ts index 50f66785b..80021a8cf 100644 --- a/docs/samples/opera/app/api/solutions/route.ts +++ b/docs/samples/opera/app/api/solutions/route.ts @@ -1,8 +1,7 @@ import { NextResponse } from "next/server" -import {SolutionState} from "../../../../types"; import { getServerSession } from 'next-auth'; -import { options } from '../../../auth/[...nextauth]/options'; -import { User } from '../../../../types'; +import { options } from '../auth/[...nextauth]/options'; +import { User } from '../../types'; export async function GET( request: Request, diff --git a/docs/samples/opera/app/targets/page.tsx b/docs/samples/opera/app/targets/page.tsx new file mode 100644 index 000000000..9ebbd6bde --- /dev/null +++ b/docs/samples/opera/app/targets/page.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { getServerSession } from 'next-auth'; +import MultiView from '@/components/MultiView'; +import { options } from '../api/auth/[...nextauth]/options'; +import {TargetState, User} from '../types'; +const getTargets = async () => { + const session = await getServerSession(options); + const symphonyApi = process.env.SYMPHONY_API; + const userObj: User | undefined = session?.user?? undefined; + const res = await fetch( `${symphonyApi}targets/registry`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${userObj?.accessToken}`, + } + }); + const data = await res.json(); + return data; +} +async function TargetsPage() { + const targets = await getTargets(); + const params = { + type: 'targets', + menuItems: [ + { + name: 'Add Solution', + href: '/solutions/add', + } + ], + views: ['cards', 'table'], + items: targets, + columns: [] + } + return ( +
+ +
+); +} + +export default TargetsPage; \ No newline at end of file diff --git a/docs/samples/opera/app/types.d.ts b/docs/samples/opera/app/types.d.ts index 2f9963e90..bb3544d54 100644 --- a/docs/samples/opera/app/types.d.ts +++ b/docs/samples/opera/app/types.d.ts @@ -64,7 +64,7 @@ export interface CatalogSpec { properties: Record; metadata: Record; parentName: string; - objectRef: ObjectRef; + objectRef?: ObjectRef | null | undefined; generation: string; } @@ -78,6 +78,79 @@ export interface CatalogState { status: CatalogStatus; } +export interface BindingSpec { + role: string; + provider: string; + config: Record; +} + +export interface TopologySpec { + device: string; + selector: Record; + bindings: BindingSpec[]; +} + +export interface TargetSpec { + displayName: string; + scope: string; + metadata: Record; + properties: Record; + components: ComponentSpec[]; + constraints: string; + topologies: TopologySpec[]; + forceRedeploy: boolean; + generation: string; + version: string; +} + +export interface ComponentError { + code: string; + message: string; + target: string; +} + +export interface TargetError { + code: string; + message: string; + target: string; + details: ComponentError[]; +} + +export interface ErrorType { + code: string; + message: string; + target: string; + details: TargetError[]; +} + +export interface ProvisioningStatus{ + operationId: string; + status: string; + failureCause: string; + logErrors: boolean; + error: ErrorType; + output: Record; +} + +export interface DeployableStatus { + properties: Record; + ProvisioningStatus: ProvisioningStatus; + lastModified: Date; +} + +export interface ObjectMeta { + namespace: string; + name: string; + labels: Record; + annotations: Record; +} + +export interface TargetState { + metadata: ObjectMeta; + spec: TargetSpec; + status: DeployableStatus; +} + export interface SolutionState { id: string; namespace: string; @@ -160,7 +233,7 @@ export interface User { email?: string | nulll | undefined; image?: string | null | undefined; username?: string; - tokenType: string; + tokenType?: string | null | undefined; roles?: string[] | undefined; } @@ -172,4 +245,4 @@ export interface Rule { } export interface Schema { rules: Record; -} \ No newline at end of file +} diff --git a/docs/samples/opera/components/Filter.tsx b/docs/samples/opera/components/Filter.tsx new file mode 100644 index 000000000..df42aa4aa --- /dev/null +++ b/docs/samples/opera/components/Filter.tsx @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; +import { Input, NavbarContent, Button } from '@nextui-org/react'; + +// Define an interface for the component props +interface FilterProps { + onSelectFilter: (filter: string) => void; // This function takes a string and returns void +} + +const Filter: React.FC = ({ onSelectFilter }) => { + const [selectedFilter, setSelectedFilter] = useState(''); // State to keep track of the filter input + + // Function to handle input changes + const handleFilterChange = (event: React.ChangeEvent) => { + const filter = event.target.value; + setSelectedFilter(filter); // Update selected filter state + onSelectFilter(filter); // Pass filter to parent component + }; + + // Function to handle filter submission + const handleFilterSubmit = () => { + onSelectFilter(selectedFilter); + }; + + return ( + + + + + ); +}; + +export default Filter; diff --git a/docs/samples/opera/components/MultiView.tsx b/docs/samples/opera/components/MultiView.tsx index 318b89056..010811259 100644 --- a/docs/samples/opera/components/MultiView.tsx +++ b/docs/samples/opera/components/MultiView.tsx @@ -17,9 +17,11 @@ import {useState} from 'react'; import CampaignCardList from "./campaigns/CampaignCardList"; import SiteCardList from "./sites/SiteCardList"; import SolutionCardList from "./solutions/SolutionCardList"; +import TargetCardList from "./targets/TargetCardList"; import SiteMap from "./sites/SiteMap"; import AssetList from "./assets/AssetList"; import GraphTable from "./graph/GraphTable"; +import Filter from "./Filter"; interface MenuInfo { name: string; @@ -47,7 +49,8 @@ interface MultiViewProps { function MultiView(props: MultiViewProps) { const { params } = props; const [selected, setSelected] = useState(""); - const [selectedColumn, setSelectedColumn] = useState(""); + const [selectedColumn, setSelectedColumn] = useState(""); + const [selectedFilter, setSelectedFilter] = useState(""); // State to hold selected filter function handleSelectionChange(key: any) { setSelected(key.toString()); @@ -57,6 +60,10 @@ function MultiView(props: MultiViewProps) { setSelectedColumn(key.toString()); } + const handleFilterChange = (filter: string) => { + setSelectedFilter(filter); // Update selected filter state + }; + return (
@@ -114,7 +121,8 @@ function MultiView(props: MultiViewProps) { ))} - )} + )} +
@@ -129,6 +137,7 @@ function MultiView(props: MultiViewProps) {
}> {view === 'cards' && params.type === 'campaigns' && } {view === 'cards' && params.type === 'solutions' && } + {view === 'cards' && params.type === 'targets' && } {view === 'cards' && params.type === 'sites' && } {view === 'map' && params.type === 'sites' && } {view === "cards" && params.type === "assets" && } diff --git a/docs/samples/opera/components/SideBar.tsx b/docs/samples/opera/components/SideBar.tsx index d1a0dd71a..b50f7c22e 100644 --- a/docs/samples/opera/components/SideBar.tsx +++ b/docs/samples/opera/components/SideBar.tsx @@ -53,7 +53,7 @@ function SideBar() { }, { name: "Targets", - href: "/coming-soon", + href: "/targets", icon: FiServer, }, { diff --git a/docs/samples/opera/components/TargetSpecCard.tsx b/docs/samples/opera/components/TargetSpecCard.tsx new file mode 100644 index 000000000..8a9cd17d3 --- /dev/null +++ b/docs/samples/opera/components/TargetSpecCard.tsx @@ -0,0 +1,104 @@ +import { TargetSpec } from '../app/types'; +import {Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Chip} from "@nextui-org/react"; +import {FaDocker} from 'react-icons/fa'; +import {SiHelm} from 'react-icons/si'; +import {SiKubernetes} from 'react-icons/si'; +import {SiWindows} from 'react-icons/si'; +import { SiGnubash } from "react-icons/si"; +import { CgListTree } from "react-icons/cg"; +interface TargetSpecCardProps { + target: TargetSpec; +} +function TargetSpecCard(props: TargetSpecCardProps) { + const { target } = props; + return ( + + + + NAME + PACKAGE + VERSION + + + {target.components && (target.components.map((component: any) => ( + + + {component.type === 'container' && ( + + )} + {component.type === 'helm.v3' && ( + + )} + {component.type === 'yaml.k8s' && ( + + )} + {component.type === 'program' && ( + + )} + {component.type === 'uwp' && ( + + )} + {component.type === 'script' && ( + + )} + + {component.name} + + {component.type === 'container' && ( + {component.properties['container.image'].split(':')[0]} + )} + {component.type === 'script' && ( + {component.properties['container.image'].split(':')[0]} + )} + {component.type === 'program' && ( + {component.properties['program.image'].split(':')[0]} + )} + {component.type === 'helm.v3' && ( + {component.properties['chart']['repo']} + )} + {component.type === 'yaml.k8s' && ( + {`[object]`} + )} + {component.type === 'uwp' && ( + {component.properties['app.image']} + )} + + + {component.type === 'container' && ( + + {component.properties['container.image'].includes(':') + ? component.properties['container.image'].split(':')[1] + : '(latest)'} + + )} + {component.type === 'script' && ( + + {component.properties['container.image'].includes(':') + ? component.properties['container.image'].split(':')[1] + : '(latest)'} + + )} + {component.type === 'program' && ( + + {component.properties['program.image'].includes(':') + ? component.properties['program.image'].split(':')[1] + : '(latest)'} + + )} + {component.type === 'helm.v3' && ( + {component.properties['chart']['version']} + )} + {component.type === 'yaml.k8s' && ( + {`n/a`} + )} + {component.type === 'uwp' && ( + {component.properties['app.version']} + )} + + + )))} + +
+ ); +} +export default TargetSpecCard; \ No newline at end of file diff --git a/docs/samples/opera/components/assets/AssetCard.tsx b/docs/samples/opera/components/assets/AssetCard.tsx index 1fc0e0f0d..8e5bd10b3 100644 --- a/docs/samples/opera/components/assets/AssetCard.tsx +++ b/docs/samples/opera/components/assets/AssetCard.tsx @@ -9,7 +9,7 @@ import { FaGithub } from 'react-icons/fa'; interface AssetCardProps { catalog: CatalogState; - refCatalog: CatalogState; + refCatalog?: CatalogState | null | undefined; } function AssetCard(props: AssetCardProps) { const { catalog, refCatalog } = props; @@ -29,10 +29,10 @@ function AssetCard(props: AssetCardProps) { - {(catalog.spec.type === 'config' && !catalog.spec.objectRef.name) && ( + {(catalog.spec.type === 'config' && !catalog.spec.objectRef?.name) && ( )} - {(catalog.spec.type === 'config' && catalog.spec.objectRef.name) && ( + {(catalog.spec.type === 'config' && catalog.spec.objectRef?.name) && (
{catalog.spec.objectRef.address}
)} {catalog.spec.type === 'solution' && ( diff --git a/docs/samples/opera/components/assets/AssetList.tsx b/docs/samples/opera/components/assets/AssetList.tsx index c0b6a339c..2de5bd949 100644 --- a/docs/samples/opera/components/assets/AssetList.tsx +++ b/docs/samples/opera/components/assets/AssetList.tsx @@ -1,27 +1,32 @@ import { CatalogState } from '../../app/types'; import AssetCard from './AssetCard'; + interface AssetListProps { catalogs: CatalogState[]; } -function AssetList(props: AssetListProps) { - const { catalogs } = props; - //create a map of catalogs - const references: any = {}; - for (const [_, cats] of Object.entries(catalogs)) { - cats.forEach((catalog: CatalogState) => { - references[catalog.spec.name] = catalog; - }); - } - const mergedCatalogs = []; - for (const [_, cats] of Object.entries(catalogs)) { - mergedCatalogs.push(...cats); - } + +function AssetList({ catalogs }: AssetListProps) { + // Create a map of catalogs by name for easy reference + const references: Record = {}; + catalogs.forEach((catalog) => { + references[catalog.spec.name] = catalog; + }); + + // If you want to merge catalogs or perform other operations, you can do it directly with the array + // Assuming mergedCatalogs is supposed to be the same as catalogs in this simplified correction + const mergedCatalogs = [...catalogs]; // This creates a shallow copy if needed, or directly use catalogs return ( -
- {mergedCatalogs.map((catalog: CatalogState) => - )} +
+ {mergedCatalogs.map((catalog) => ( + + ))}
); } -export default AssetList; \ No newline at end of file + +export default AssetList; diff --git a/docs/samples/opera/components/catalogs/CatalogCard.tsx b/docs/samples/opera/components/catalogs/CatalogCard.tsx index 1a31dfab0..46b2892f5 100644 --- a/docs/samples/opera/components/catalogs/CatalogCard.tsx +++ b/docs/samples/opera/components/catalogs/CatalogCard.tsx @@ -29,10 +29,10 @@ function CatalogCard(props: CatalogCardProps) { - {(catalog.spec.type === 'config' && !catalog.spec.objectRef.name) && ( + {(catalog.spec.type === 'config' && !catalog.spec.objectRef?.name) && ( )} - {(catalog.spec.type === 'config' && catalog.spec.objectRef.name) && ( + {(catalog.spec.type === 'config' && catalog.spec.objectRef?.name) && (
{catalog.spec.objectRef.address}
)} {catalog.spec.type === 'solution' && ( diff --git a/docs/samples/opera/components/editors/CatalogEditor.tsx b/docs/samples/opera/components/editors/CatalogEditor.tsx index e3e87349d..637a11999 100644 --- a/docs/samples/opera/components/editors/CatalogEditor.tsx +++ b/docs/samples/opera/components/editors/CatalogEditor.tsx @@ -11,11 +11,23 @@ interface CatalogEditorProps { schemas: CatalogState[]; } +interface Field { + name: string; +} + +interface Fields { + [key: string]: Field; +} + +interface Error { + error: string; +} + function CatalogEditor(props: CatalogEditorProps) { const { schemas } = props; - const [fields, setFields] = useState({}); - const [errors, setErrors] = useState({}); - const [moreFields, setMoreFields] = useState({}); + const [fields, setFields] = useState>({}); + const [errors, setErrors] = useState>({}); + const [moreFields, setMoreFields] = useState({}); useEffect(() => { if (schemas.length == 0) { schemaSelected(schemas[0].spec.name); @@ -41,21 +53,26 @@ function CatalogEditor(props: CatalogEditorProps) { event.preventDefault(); const formData = new FormData(event.currentTarget); const data = Object.fromEntries(formData.entries()); - const catalog = { - siteId : process.env.SYMPHONY_SITE, - name: data.name, - type: "config" + const catalog: CatalogSpec = { + siteId: process.env.SYMPHONY_SITE || '', + name: typeof data.name === 'string' ? data.name : '', + parentName: "", + type: "config", + metadata: {}, + properties: {}, + generation: "" }; + if (data.schema) { catalog.metadata = { - "schema": data.schema + "schema": typeof data.schema === 'string' ? data.schema: '' } } catalog.properties = {}; Object.keys(data).forEach((key: string) => { if (key.includes("-name")) { const id = key.split("-")[0]; - const name = data[key]; + const name: string = typeof data[key] === 'string' ? data[key] as string: ''; const value = data[`${id}-value`]; if (name && value) { catalog.properties[name] = value; @@ -101,7 +118,8 @@ function CatalogEditor(props: CatalogEditorProps) { const removeRow = (event: React.MouseEvent) => { event.preventDefault(); const newFields = {...moreFields}; - const id = event.target.id; + const target = event.target as HTMLElement; + const id = target.id; delete newFields[id]; setMoreFields(newFields); } diff --git a/docs/samples/opera/components/graph/GraphTable.tsx b/docs/samples/opera/components/graph/GraphTable.tsx index 0d0061564..d11606fff 100644 --- a/docs/samples/opera/components/graph/GraphTable.tsx +++ b/docs/samples/opera/components/graph/GraphTable.tsx @@ -56,7 +56,7 @@ function BuildTreeNodeLabel(catalog: CatalogState) { function GraphTable(props: GraphTableProps) { const [visibleNodes, setVisibleNodes] = useState([]); const { catalogs, columns } = props; - const treeViewRef = useRef(null); + const treeViewRef = useRef(null); const updateVisibleNodes = () => { const visibleNodes: string[] = []; @@ -68,17 +68,18 @@ function GraphTable(props: GraphTableProps) { setVisibleNodes(visibleNodes); } - const mergedCatalogs = []; + const mergedCatalogs: CatalogState[] = []; - const mergedColumns = []; + const mergedColumns: any[] = []; if (columns) { for (const [_, cols] of Object.entries(columns)) { mergedColumns.push(cols); } } - for (const [_, cats] of Object.entries(catalogs)) { - mergedCatalogs.push(...cats); - } + // for (const [_, cats] of Object.entries(catalogs)) { + // mergedCatalogs.push(...cats); + // } + mergedCatalogs.push(...catalogs); useEffect(() => { const observer = new MutationObserver(() => { @@ -120,7 +121,7 @@ function GraphTable(props: GraphTableProps) { updateVisibleNodes(); }; - const treeNodes = BuildForest(catalogs); + const treeNodes = BuildForest({"FIX-ME": catalogs}); if (mergedColumns?.length) { return (
@@ -145,6 +146,7 @@ function GraphTable(props: GraphTableProps) { )} + diff --git a/docs/samples/opera/components/solutions/SolutionCard.tsx b/docs/samples/opera/components/solutions/SolutionCard.tsx index 3d0b46b99..5e30b420d 100644 --- a/docs/samples/opera/components/solutions/SolutionCard.tsx +++ b/docs/samples/opera/components/solutions/SolutionCard.tsx @@ -32,8 +32,8 @@ function SolutionCard(props: SolutionCardProps) { const { solution } = props; const [activeView, setActiveView] = useState('properties'); - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); // const initialNodes = [ // { id: '1', position: { x: 0, y: 0 }, data: { label: '1' } }, diff --git a/docs/samples/opera/components/targets/TargetCard.tsx b/docs/samples/opera/components/targets/TargetCard.tsx new file mode 100644 index 000000000..c0c3eec2b --- /dev/null +++ b/docs/samples/opera/components/targets/TargetCard.tsx @@ -0,0 +1,73 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +'use client'; + +import {Card, CardHeader, CardBody, CardFooter, Divider} from '@nextui-org/react'; +import {TargetState} from '../../app/types'; +import {useState} from 'react'; +import ReactFlow from 'reactflow'; +import 'reactflow/dist/style.css'; +import {LuFileJson2} from 'react-icons/lu'; +import TargetSpecCard from "../TargetSpecCard"; +import {Tabs, Tab} from "@nextui-org/react"; +import { FcOk } from "react-icons/fc"; +import { FcHighPriority } from "react-icons/fc"; + +interface TargetCardProps { + target: TargetState; +} +function TargetCard(props: TargetCardProps) { + + const { target } = props; + const [activeView, setActiveView] = useState('properties'); + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + + const json = JSON.stringify(target, null, 2); + + const updateActiveView = (key: any) => { + setActiveView(key.toString()); + } + + return ( + + + {target.metadata.name} + + + + + + + + {activeView == 'properties' && ( + + )} + {activeView == 'json' && ( +
{json}
+ )} +
+ + +
+ {target.status.properties && target.status.properties.status === 'Succeeded' && ( + + OK + + )} + {target.status.properties && target.status.properties.status != 'Succeeded' && ( + + Failed + + )} +
+
+
+ ); +} + +export default TargetCard; \ No newline at end of file diff --git a/docs/samples/opera/components/targets/TargetCardList.tsx b/docs/samples/opera/components/targets/TargetCardList.tsx new file mode 100644 index 000000000..0047b473e --- /dev/null +++ b/docs/samples/opera/components/targets/TargetCardList.tsx @@ -0,0 +1,32 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +import {TargetState} from '../../app/types'; +import TargetCard from './TargetCard'; + +interface TargetCardListProps { + targets: TargetState[]; + filter?: string; +} + +function TargetCardList(props: TargetCardListProps) { + const { targets, filter } = props; + if (!targets) { + return (
No data
); + } + + const filteredTargets = filter ? targets.filter(target => target.spec.properties && target.spec.properties.scenario === filter) : targets; + + return ( +
+ {filteredTargets.map((target: any) => { + return ; + })} +
+ ); +} + +export default TargetCardList; \ No newline at end of file diff --git a/docs/samples/opera/package-lock.json b/docs/samples/opera/package-lock.json index 020291e02..8579f10fd 100644 --- a/docs/samples/opera/package-lock.json +++ b/docs/samples/opera/package-lock.json @@ -13,6 +13,7 @@ "@mui/lab": "^5.0.0-alpha.141", "@mui/material": "^5.14.6", "@nextui-org/react": "^2.0.22", + "@nextui-org/theme": "^2.2.3", "@react-google-maps/api": "^2.19.2", "@types/node": "20.5.0", "@types/react": "18.2.20", @@ -26,7 +27,7 @@ "react-dom": "^18.2.0", "react-icons": "^4.10.1", "reactflow": "^11.10.3", - "tailwindcss": "3.3.3", + "tailwindcss": "^3.3.3", "typescript": "5.1.6" } }, @@ -42,61 +43,62 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dependencies": { - "@babel/highlight": "^7.22.10", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.24.5", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -105,12 +107,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", - "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -135,11 +137,6 @@ "stylis": "4.2.0" } }, - "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, "node_modules/@emotion/cache": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", @@ -152,40 +149,33 @@ "stylis": "4.2.0" } }, - "node_modules/@emotion/cache/node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, "node_modules/@emotion/hash": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", "dependencies": { - "@emotion/memoize": "0.7.4" + "@emotion/memoize": "^0.8.1" } }, "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", - "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.2", + "@emotion/serialize": "^1.1.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", "@emotion/utils": "^1.2.1", "@emotion/weak-memoize": "^0.3.1", @@ -201,9 +191,9 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", - "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", "dependencies": { "@emotion/hash": "^0.9.1", "@emotion/memoize": "^0.8.1", @@ -212,25 +202,20 @@ "csstype": "^3.0.2" } }, - "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, "node_modules/@emotion/sheet": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "node_modules/@emotion/styled": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", - "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "version": "11.11.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", + "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", - "@emotion/is-prop-valid": "^1.2.1", - "@emotion/serialize": "^1.1.2", + "@emotion/is-prop-valid": "^1.2.2", + "@emotion/serialize": "^1.1.4", "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", "@emotion/utils": "^1.2.1" }, @@ -244,19 +229,6 @@ } } }, - "node_modules/@emotion/styled/node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@emotion/styled/node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", @@ -281,28 +253,28 @@ "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, "node_modules/@floating-ui/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", - "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.1.tgz", + "integrity": "sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==", "dependencies": { - "@floating-ui/utils": "^0.1.1" + "@floating-ui/utils": "^0.2.0" } }, "node_modules/@floating-ui/dom": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz", - "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.4.tgz", + "integrity": "sha512-0G8R+zOvQsAG1pg2Q99P21jiqxqGBW1iRe/iXHsBRBxnpXKFI8QwbB4x5KmYLggNO5m34IQgOIu9SCRfR/WWiQ==", "dependencies": { - "@floating-ui/core": "^1.4.1", - "@floating-ui/utils": "^0.1.1" + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", - "integrity": "sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.9.tgz", + "integrity": "sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==", "dependencies": { - "@floating-ui/dom": "^1.3.0" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -310,16 +282,16 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", - "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" }, "node_modules/@formatjs/ecma402-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", - "integrity": "sha512-6ueQTeJZtwKjmh23bdkq/DMqH4l4bmfvtQH98blOSbiXv/OUiyijSW6jU22IT8BNM1ujCaEvJfTtyCYVH38EMQ==", + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", + "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", "dependencies": { - "@formatjs/intl-localematcher": "0.4.0", + "@formatjs/intl-localematcher": "0.5.4", "tslib": "^2.4.0" } }, @@ -332,28 +304,28 @@ } }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.0.tgz", - "integrity": "sha512-yT6at0qc0DANw9qM/TU8RZaCtfDXtj4pZM/IC2WnVU80yAcliS3KVDiuUt4jSQAeFL9JS5bc2hARnFmjPdA6qw==", + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.6.tgz", + "integrity": "sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", - "@formatjs/icu-skeleton-parser": "1.6.0", + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/icu-skeleton-parser": "1.8.0", "tslib": "^2.4.0" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.0.tgz", - "integrity": "sha512-eMmxNpoX/J1IPUjPGSZwo0Wh+7CEvdEMddP2Jxg1gQJXfGfht/FdW2D5XDFj3VMbOTUQlDIdZJY7uC6O6gjPoA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.0.tgz", + "integrity": "sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", + "@formatjs/ecma402-abstract": "1.18.2", "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-localematcher": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz", - "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", "dependencies": { "tslib": "^2.4.0" } @@ -367,72 +339,88 @@ } }, "node_modules/@googlemaps/markerclusterer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.3.2.tgz", - "integrity": "sha512-zb9OQP8XscZp2Npt1uQUYnGKu1miuq4DPP28JyDuFd6HV17HCEcjV9MtBi4muG/iVRXXvuHW9bRCnHbao9ITfw==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz", + "integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==", "dependencies": { "fast-deep-equal": "^3.1.3", "supercluster": "^8.0.1" } }, "node_modules/@internationalized/date": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.4.0.tgz", - "integrity": "sha512-QUDSGCsvrEVITVf+kv9VSAraAmCgjQmU5CiXtesUBBhBe374NmnEIIaOFBZ72t29dfGMBP0zF+v6toVnbcc6jg==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.3.tgz", + "integrity": "sha512-X9bi8NAEHAjD8yzmPYT2pdJsbe+tYSEBAfowtlxJVJdZR3aK8Vg7ZUT1Fm5M47KLzp/M1p1VwAaeSma3RT7biw==", "dependencies": { "@swc/helpers": "^0.5.0" } }, "node_modules/@internationalized/message": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.1.tgz", - "integrity": "sha512-ZgHxf5HAPIaR0th+w0RUD62yF6vxitjlprSxmLJ1tam7FOekqRSDELMg4Cr/DdszG5YLsp5BG3FgHgqquQZbqw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.3.tgz", + "integrity": "sha512-jba3kGxnh4hN4zoeJZuMft99Ly1zbmon4fyDz3VAmO39Kb5Aw+usGub7oU/sGoBIcVQ7REEwsvjIWtIO1nitbw==", "dependencies": { "@swc/helpers": "^0.5.0", "intl-messageformat": "^10.1.0" } }, "node_modules/@internationalized/number": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.2.1.tgz", - "integrity": "sha512-hK30sfBlmB1aIe3/OwAPg9Ey0DjjXvHEiGVhNaOiBJl31G0B6wMaX8BN3ibzdlpyRNE9p7X+3EBONmxtJO9Yfg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.5.2.tgz", + "integrity": "sha512-4FGHTi0rOEX1giSkt5MH4/te0eHBq3cvAYsfLlpguV6pzJAReXymiYpE5wPCqKqjkUO3PIsyvk+tBiIV1pZtbA==", "dependencies": { "@swc/helpers": "^0.5.0" } }, "node_modules/@internationalized/string": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.1.1.tgz", - "integrity": "sha512-fvSr6YRoVPgONiVIUhgCmIAlifMVCeej/snPZVzbzRPxGpHl3o1GRe+d/qh92D8KhgOciruDUH8I5mjdfdjzfA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.2.tgz", + "integrity": "sha512-5xy2JfSQyGqL9FDIdJXVjoKSBBDJR4lvwoCbqKhc5hQZ/qSLU/OlONCmrJPcSH0zxh88lXJMzbOAk8gJ48JBFw==", "dependencies": { "@swc/helpers": "^0.5.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -443,35 +431,33 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@mui/base": { - "version": "5.0.0-beta.12", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.12.tgz", - "integrity": "sha512-tZjjXNAyUpwSDT1uRliZMhRQkWYzELJ8Qi61EuOMRpi36HIwnK2T7Nr4RI423Sv8G2EEikDAZj7je33eNd73NQ==", - "dependencies": { - "@babel/runtime": "^7.22.10", - "@emotion/is-prop-valid": "^1.2.1", - "@floating-ui/react-dom": "^2.0.1", - "@mui/types": "^7.2.4", - "@mui/utils": "^5.14.6", + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "@popperjs/core": "^2.11.8", - "clsx": "^2.0.0", - "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "clsx": "^2.1.0", + "prop-types": "^15.8.1" }, "engines": { "node": ">=12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", @@ -484,61 +470,39 @@ } } }, - "node_modules/@mui/base/node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@mui/base/node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, - "node_modules/@mui/base/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.14.6", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.6.tgz", - "integrity": "sha512-QZEU3pyGWLuaHbxvOlShol7U1FVgzWBR0OH9H8D7L8w4/vto5N5jJVvlqFQS3T0zbR6YGHxFaiL6Ky87jQg7aw==", + "version": "5.15.16", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.16.tgz", + "integrity": "sha512-PTIbMJs5C/vYMfyJNW8ArOezh4eyHkg2pTeA7bBxh2kLP1Uzs0Nm+krXWbWGJPwTWjM8EhnDrr4aCF26+2oleg==", "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/lab": { - "version": "5.0.0-alpha.141", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.141.tgz", - "integrity": "sha512-PsW55xX2ieNLldca2hLxL1SYtZgRQv++lj1W/Jyi5Z2MHuFDcdqI7yKGrOzyIWw7ctQrmHa1FTShBiCa2wkEoQ==", - "dependencies": { - "@babel/runtime": "^7.22.10", - "@mui/base": "5.0.0-beta.12", - "@mui/system": "^5.14.6", - "@mui/types": "^7.2.4", - "@mui/utils": "^5.14.6", - "clsx": "^2.0.0", - "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "version": "5.0.0-alpha.170", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.170.tgz", + "integrity": "sha512-0bDVECGmrNjd3+bLdcLiwYZ0O4HP5j5WSQm5DV6iA/Z9kr8O6AnvZ1bv9ImQbbX7Gj3pX4o43EKwCutj3EQxQg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/system": "^5.15.15", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" }, "engines": { "node": ">=12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material": "^5.0.0", + "@mui/material": ">=5.15.0", "@types/react": "^17.0.0 || ^18.0.0", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0" @@ -555,28 +519,20 @@ } } }, - "node_modules/@mui/lab/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/@mui/material": { - "version": "5.14.6", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.6.tgz", - "integrity": "sha512-C3UgGrmtvcGkQkm0ONBU7bTdapTjQc2Se3b2354xMmU7lgSgW7VM6EP9wIH5XqqoJ60m9l/s9kbTWX0Y+EaWvA==", - "dependencies": { - "@babel/runtime": "^7.22.10", - "@mui/base": "5.0.0-beta.12", - "@mui/core-downloads-tracker": "^5.14.6", - "@mui/system": "^5.14.6", - "@mui/types": "^7.2.4", - "@mui/utils": "^5.14.6", - "@types/react-transition-group": "^4.4.6", - "clsx": "^2.0.0", - "csstype": "^3.1.2", + "version": "5.15.16", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.16.tgz", + "integrity": "sha512-ery2hFReewko9gpDBqOr2VmXwQG9ifXofPhGzIx09/b9JqCQC/06kZXZDGGrOTpIddK9HlIf4yrS+G70jPAzUQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.15.16", + "@mui/system": "^5.15.15", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", "prop-types": "^15.8.1", "react-is": "^18.2.0", "react-transition-group": "^4.4.5" @@ -586,7 +542,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.5.0", @@ -607,21 +563,13 @@ } } }, - "node_modules/@mui/material/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/@mui/private-theming": { - "version": "5.14.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.6.tgz", - "integrity": "sha512-3VBLFGizBXfofyk33bwRg6t9L648aKnLmOKPfY1wFuiXq3AEYwobK65iDci/tHKxm/VKbZ6A7PFjLejvB3EvRQ==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", + "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", "dependencies": { - "@babel/runtime": "^7.22.10", - "@mui/utils": "^5.14.6", + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.14", "prop-types": "^15.8.1" }, "engines": { @@ -629,7 +577,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", @@ -642,13 +590,13 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.14.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.6.tgz", - "integrity": "sha512-I6zeu/OP1Hk4NsX1Oj85TiYl1dER0JMsLJVn76J1Ihl24A5EbiZQKJp3Mn+ufA79ypkdAvM9aQCAQyiVBFcUHg==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", + "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", "dependencies": { - "@babel/runtime": "^7.22.10", + "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", - "csstype": "^3.1.2", + "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { @@ -656,7 +604,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.4.1", @@ -673,17 +621,17 @@ } }, "node_modules/@mui/system": { - "version": "5.14.6", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.6.tgz", - "integrity": "sha512-/n0ae1MegWjiV1BpRU8jgg4E0zBjeB2VYsT/68ag/xaDuq3/TaDKJeT9REIvyBvwlG3CI3S2O+tRELktxCD1kg==", - "dependencies": { - "@babel/runtime": "^7.22.10", - "@mui/private-theming": "^5.14.6", - "@mui/styled-engine": "^5.14.6", - "@mui/types": "^7.2.4", - "@mui/utils": "^5.14.6", - "clsx": "^2.0.0", - "csstype": "^3.1.2", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", + "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.14", + "@mui/styled-engine": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { @@ -691,7 +639,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.5.0", @@ -711,20 +659,12 @@ } } }, - "node_modules/@mui/system/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/@mui/types": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", - "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==", + "version": "7.2.14", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", + "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", "peerDependencies": { - "@types/react": "*" + "@types/react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -733,13 +673,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.14.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.6.tgz", - "integrity": "sha512-AznpqLu6hrFnpHgcvsSSMCG+cDbkcCYfo+daUwBVReNYv4l+NQ8+wvBAF4aUMi155N7xWbbgh0cyKs6Wdsm3aA==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", + "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", "dependencies": { - "@babel/runtime": "^7.22.10", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^18.2.1", + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -748,21 +687,27 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@next/env": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.17.tgz", - "integrity": "sha512-rSGmt25Wxk0vGzZxDWBLE8jVW/C/JN20P3IhHc2tKVajEGy/oxStD9PbqcbCz6yOub82jYAWLqnoMITnssB+3g==" + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", + "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==" }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.17.tgz", - "integrity": "sha512-dzsHchMmBwa6w6Gf5sp5+WmVt9/H4KWdSHy45aFE/UNmgr9V9eKfTW29k9Np9glLCEzrwnU1MztbAqDrnV9gEA==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz", + "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==", "cpu": [ "arm64" ], @@ -775,9 +720,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.17.tgz", - "integrity": "sha512-iH6UpMj3S40cZkJwYD+uBsAYACNu9TUCae47q2kqx1WzO3JuN/m5Zg22Cpwum/HLRJUa7ysJva/FG2noXbI0yw==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz", + "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==", "cpu": [ "x64" ], @@ -790,9 +735,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.17.tgz", - "integrity": "sha512-yj3YKGkSg52GL+4XhdfidibYJoq/5pYkQAc8Z4Q1e1nJ7CTOKn4KobTDLXqC5QVJncQRxC2u6vGaMLBe2UUa5Q==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz", + "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==", "cpu": [ "arm64" ], @@ -805,9 +750,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.17.tgz", - "integrity": "sha512-w8+8ShThIgIgIkLk22NY+ZMF/yf5Dl6+tqOaNUzXy6b0gQSwtpVb0t4eSTx2VUqRxLl36dv9cqomGbthvuPiGA==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz", + "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==", "cpu": [ "arm64" ], @@ -820,9 +765,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.17.tgz", - "integrity": "sha512-IQlJNdxvfqgHxJU6ITERf9qaA0m6mRo/gD0al/5CcXvs6cDihR/UzI09Bc+3vQSJV3ACAzrZjsF7dtdzVutvog==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz", + "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==", "cpu": [ "x64" ], @@ -835,9 +780,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.17.tgz", - "integrity": "sha512-retAiJGtOS216pqAcNOwBUOqgqDH7kYzzj4jLrfVcb/sCQJ+JawMwayc3LEbpvMDZx8CHLECcs6bB45mMxkZEw==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz", + "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==", "cpu": [ "x64" ], @@ -850,9 +795,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.17.tgz", - "integrity": "sha512-PtaemQL9rwoRtS6kgjXxfRQLUbzBmtMxaXZTBnKnb+EjrDFkC+YI82kktL97LMrHRGQsMJcBQQtNQDJCBJmu2Q==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz", + "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==", "cpu": [ "arm64" ], @@ -865,9 +810,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.17.tgz", - "integrity": "sha512-5jJVxit2B3g/zRWJJ6/YeMHBch7PL10O5qR5BZyuFCoO/bg6MPtz5+U+FvbVCSgCKePU19lRGNsyX+BAu/V+vw==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz", + "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==", "cpu": [ "ia32" ], @@ -880,9 +825,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.17.tgz", - "integrity": "sha512-3QOf2LfziycZW1iVgiA63xVVUMkawurZJ/jwgBqziUNw4r8XHLenNTgbn5XcdHqKuZKUuLSi/6v1/4myGWM0GA==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz", + "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==", "cpu": [ "x64" ], @@ -894,704 +839,1117 @@ "node": ">= 10" } }, - "node_modules/@nextui-org/accordion": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@nextui-org/accordion/-/accordion-2.0.12.tgz", - "integrity": "sha512-pD0E4Y/b/cJKGmk0iXjJ72UaMEou3hCbShDMa9KBx/iqei61e4WYJ4VcrzzmuH7rN5uTApCGa2bciYnWcGCa4A==", - "dependencies": { - "@nextui-org/aria-utils": "2.0.5", - "@nextui-org/divider": "2.0.10", - "@nextui-org/framer-transitions": "2.0.5", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-icons": "2.0.2", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-aria-accordion-item": "2.0.3", - "@react-aria/accordion": "3.0.0-alpha.20", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/tree": "^3.7.1" + "node_modules/@nextui-org/aria-utils": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@nextui-org/aria-utils/-/aria-utils-2.0.18.tgz", + "integrity": "sha512-9ZIZgWFU26csBnfAxsG5HEcz/nLmbeUusbi3kME3sm69iu5B0+A0WSABW+Ffk1Vhtyh73zJZRpA8baC673+5tQ==", + "dependencies": { + "@nextui-org/react-rsc-utils": "2.0.12", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/system": "2.1.2", + "@react-aria/utils": "^3.23.2", + "@react-stately/collections": "^3.10.5", + "@react-types/overlays": "^3.8.5", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { - "framer-motion": ">=4.0.0", - "react": ">=18" + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/aria-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nextui-org/aria-utils/-/aria-utils-2.0.5.tgz", - "integrity": "sha512-pbfSGjQeGQoWMVHE9iCOPq2MSLPa2AIZwazdBO1QKyzfXGRCoJ6FM1zX+EvBrax/8+6DtZb9bZ6UzICr5ZLvVw==", + "node_modules/@nextui-org/framer-utils": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@nextui-org/framer-utils/-/framer-utils-2.0.18.tgz", + "integrity": "sha512-RNI5/wKjgLNjEaVdLrXH8J/mkC7HKZ6S99JNFmviU1JiVgWzwHKtuci5ZPDntUFGg6G8kX6P7OCDh+d/pMJQAA==", "dependencies": { - "@nextui-org/system": "2.0.5", - "@react-aria/utils": "^3.19.0", - "@react-stately/collections": "^3.10.0", - "@react-types/overlays": "^3.8.1", - "@react-types/shared": "^3.19.0" + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/system": "2.1.2", + "@nextui-org/use-measure": "2.0.1" }, "peerDependencies": { - "react": ">=18" + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/avatar": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/avatar/-/avatar-2.0.11.tgz", - "integrity": "sha512-AAbZrTyyiLbJJZRs94HRilg7J/YF42NBtPQf+GfGvgtjc6pFFrLwlKZrRKBqChx+nzvrkDdmiCFBp6t+MBRIvg==", - "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-image": "2.0.2", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0" + "node_modules/@nextui-org/react": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@nextui-org/react/-/react-2.3.6.tgz", + "integrity": "sha512-mA3IgPBmVZLpwLxn1t97fpwjBL+dZdAt4x9+3TjJfEQjbH9j/FvUsOAIpaT53BMcDIWrqP3Co3yR+AbplgSiEg==", + "dependencies": { + "@nextui-org/accordion": "2.0.32", + "@nextui-org/autocomplete": "2.0.16", + "@nextui-org/avatar": "2.0.27", + "@nextui-org/badge": "2.0.27", + "@nextui-org/breadcrumbs": "2.0.7", + "@nextui-org/button": "2.0.31", + "@nextui-org/calendar": "2.0.4", + "@nextui-org/card": "2.0.28", + "@nextui-org/checkbox": "2.0.29", + "@nextui-org/chip": "2.0.28", + "@nextui-org/code": "2.0.27", + "@nextui-org/date-input": "2.0.3", + "@nextui-org/date-picker": "2.0.7", + "@nextui-org/divider": "2.0.27", + "@nextui-org/dropdown": "2.1.23", + "@nextui-org/framer-utils": "2.0.18", + "@nextui-org/image": "2.0.27", + "@nextui-org/input": "2.1.21", + "@nextui-org/kbd": "2.0.28", + "@nextui-org/link": "2.0.29", + "@nextui-org/listbox": "2.1.19", + "@nextui-org/menu": "2.0.22", + "@nextui-org/modal": "2.0.33", + "@nextui-org/navbar": "2.0.30", + "@nextui-org/pagination": "2.0.30", + "@nextui-org/popover": "2.1.21", + "@nextui-org/progress": "2.0.28", + "@nextui-org/radio": "2.0.28", + "@nextui-org/ripple": "2.0.28", + "@nextui-org/scroll-shadow": "2.1.16", + "@nextui-org/select": "2.1.27", + "@nextui-org/skeleton": "2.0.27", + "@nextui-org/slider": "2.2.9", + "@nextui-org/snippet": "2.0.35", + "@nextui-org/spacer": "2.0.27", + "@nextui-org/spinner": "2.0.28", + "@nextui-org/switch": "2.0.28", + "@nextui-org/system": "2.1.2", + "@nextui-org/table": "2.0.33", + "@nextui-org/tabs": "2.0.29", + "@nextui-org/theme": "2.2.3", + "@nextui-org/tooltip": "2.0.33", + "@nextui-org/user": "2.0.28", + "@react-aria/visually-hidden": "^3.8.10" }, "peerDependencies": { - "react": ">=18" + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/badge": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@nextui-org/badge/-/badge-2.0.9.tgz", - "integrity": "sha512-cKmFJTOCGa2jbRSHecfveO4By0AlsSiosP+FBK2dm9SxjCbyyONLBUHNwEmCn7US/TdBnKJYW79rpkkplptbSA==", + "node_modules/@nextui-org/react-rsc-utils": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@nextui-org/react-rsc-utils/-/react-rsc-utils-2.0.12.tgz", + "integrity": "sha512-s2IG4pM1K+kbm6A2g3UpqrS592AExpGixtZNPJ2lV5+UQi1ld3vb4EiBIOViZMoSCNCoNdaeO5Yqo6cKghwCPA==" + }, + "node_modules/@nextui-org/react-utils": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@nextui-org/react-utils/-/react-utils-2.0.13.tgz", + "integrity": "sha512-4DM1Cph1lVY64T/HDyEqcxYkInXx6hdL1Kp9StLza9yqgYmVipTaPkWZdmWbfkhP+dVVqrH3DVFfHtpLTQ625w==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system-rsc": "2.0.3", - "@nextui-org/theme": "2.0.5" + "@nextui-org/react-rsc-utils": "2.0.12", + "@nextui-org/shared-utils": "2.0.5" }, "peerDependencies": { "react": ">=18" } }, - "node_modules/@nextui-org/button": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/button/-/button-2.0.11.tgz", - "integrity": "sha512-cIxnJ0T7zK24SEBenfpef2ln+Bbfg0m45scy1GdaPON8lFX7zr8hiHjaP4g/1aTqxujNOnGhanRS/Zu4m7NhOA==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/accordion": { + "version": "2.0.32", + "resolved": "https://registry.npmjs.org/@nextui-org/accordion/-/accordion-2.0.32.tgz", + "integrity": "sha512-iwvEd89SdOrtCxeX2Pq44wmgFm6a01sCq79BgCKuqMcsCFekZ5/yQu09R3kBB6Kne4ghZWF6MXgmzOgbS04atg==", + "dependencies": { + "@nextui-org/aria-utils": "2.0.18", + "@nextui-org/divider": "2.0.27", + "@nextui-org/framer-utils": "2.0.18", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-aria-accordion": "2.0.4", + "@react-aria/button": "^3.9.3", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-stately/tree": "^3.7.6", + "@react-types/accordion": "3.0.0-alpha.19", + "@react-types/shared": "^3.22.1" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/autocomplete": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@nextui-org/autocomplete/-/autocomplete-2.0.16.tgz", + "integrity": "sha512-cVkFTiiM6Io7XPKMMdNZdTg9OpC/SVOsO48RrbxIv9Nl2HzvQYadhsiYett3skSMTy4u3Az8FJPUp+ql0GmxxA==", + "dependencies": { + "@nextui-org/aria-utils": "2.0.18", + "@nextui-org/button": "2.0.31", + "@nextui-org/input": "2.1.21", + "@nextui-org/listbox": "2.1.19", + "@nextui-org/popover": "2.1.21", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/scroll-shadow": "2.1.16", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/spinner": "2.0.28", + "@nextui-org/use-aria-button": "2.0.7", + "@nextui-org/use-safe-layout-effect": "2.0.5", + "@react-aria/combobox": "^3.8.4", + "@react-aria/focus": "^3.16.2", + "@react-aria/i18n": "^3.10.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-aria/visually-hidden": "^3.8.10", + "@react-stately/combobox": "^3.8.2", + "@react-types/combobox": "^3.10.1", + "@react-types/shared": "^3.22.1" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/avatar": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/avatar/-/avatar-2.0.27.tgz", + "integrity": "sha512-rmEWhzg7bHOYWCvcFWBjex80aRtnLE7QyHWTHr9+KtOQRJRtv33Kxy5JfDcCQ6vKBz/ZPAWJ76ftUaba3yvXjQ==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/ripple": "2.0.11", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/spinner": "2.0.9", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-aria-button": "2.0.3", - "@react-aria/button": "^3.8.1", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-types/button": "^3.7.4", - "@react-types/shared": "^3.19.0" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-image": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/card": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/card/-/card-2.0.11.tgz", - "integrity": "sha512-V/4cCuRfteMnCA7bBYUw7cQKZPNivSWpQgIHwCPbNVH1BkEcVdoq3IIa4FlFTHnWZGvjq6CTKBnFMVHOViO6cg==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/badge": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/badge/-/badge-2.0.27.tgz", + "integrity": "sha512-7JH8X7F4FvsPjygToTId87/syh0ZPS6GK8z3zCZHu7zgA10FrwbCyQGuTpznF2GAnmtW3DxTWpemOOJD0dMJbQ==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/ripple": "2.0.11", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-aria-button": "2.0.3", - "@react-aria/button": "^3.8.1", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-types/shared": "^3.19.0" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/system-rsc": "2.1.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/checkbox": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@nextui-org/checkbox/-/checkbox-2.0.12.tgz", - "integrity": "sha512-hjtAxKvTEEnTGzGQ0SEgL0YEBemJ1b7iRkV1USrHM6g36PcN9XlSD4yVJcTU6gI0DgABHw027LWpkWbj0yfq/g==", - "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@react-aria/checkbox": "^3.10.0", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-aria/visually-hidden": "^3.8.3", - "@react-stately/checkbox": "^3.4.4", - "@react-stately/toggle": "^3.6.1", - "@react-types/checkbox": "^3.5.0", - "@react-types/shared": "^3.19.0" + "node_modules/@nextui-org/react/node_modules/@nextui-org/breadcrumbs": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@nextui-org/breadcrumbs/-/breadcrumbs-2.0.7.tgz", + "integrity": "sha512-4xD3hUy5QFtYSZWxjY8Cprq4BpSPfqkR9RyVmG9q5MCeJ8zJQTZlEZ1VCZjnwx4Mtif4kDxAgEm/eBhn6dW7mA==", + "dependencies": { + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@react-aria/breadcrumbs": "^3.5.11", + "@react-aria/focus": "^3.16.2", + "@react-aria/utils": "^3.23.2", + "@react-types/breadcrumbs": "^3.7.3", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/chip": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/chip/-/chip-2.0.11.tgz", - "integrity": "sha512-jD6FcKS9k8b7cFA3PoNmSLdeqXqHQz1qFJ63LTKhbwJ5jV/DWzwIbfPFKN8uEmHGdQmbGSp45qcyCQj6jAcKRQ==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/button": { + "version": "2.0.31", + "resolved": "https://registry.npmjs.org/@nextui-org/button/-/button-2.0.31.tgz", + "integrity": "sha512-EqrmTLhJaIFqDCK247XHuEE0c10A1mnRpIoMEgwP5GUjAFC/5itpdU80zRDi4zWXUaI6ppaVpZqWnDOCK5Qvwg==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-icons": "2.0.2", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-types/checkbox": "^3.5.0" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/ripple": "2.0.28", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/spinner": "2.0.28", + "@nextui-org/use-aria-button": "2.0.7", + "@react-aria/button": "^3.9.3", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-types/button": "^3.9.2", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/code": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@nextui-org/code/-/code-2.0.9.tgz", - "integrity": "sha512-AO25ueB9hEpwdoL0njoSRKBxmu/5JED+HzV6SH/HhCKtJN0kMk6WpIHTNp2Rl69paa2H5wlt3dnwNQRXMs8Lqg==", - "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system-rsc": "2.0.3", - "@nextui-org/theme": "2.0.5" + "node_modules/@nextui-org/react/node_modules/@nextui-org/calendar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nextui-org/calendar/-/calendar-2.0.4.tgz", + "integrity": "sha512-B1OqFBt9Z8jh42qPW6u5W0fsyf1iYs2d1hdhHfVEvFgK7E1KoNaVe03pwZsZV/tYTW/Mh5zSuNwWhhWxphzrHA==", + "dependencies": { + "@internationalized/date": "^3.5.2", + "@nextui-org/button": "2.0.31", + "@nextui-org/framer-utils": "2.0.18", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-aria-button": "2.0.7", + "@react-aria/calendar": "3.5.1", + "@react-aria/focus": "^3.14.3", + "@react-aria/i18n": "^3.8.4", + "@react-aria/interactions": "^3.19.1", + "@react-aria/utils": "^3.21.1", + "@react-aria/visually-hidden": "^3.8.6", + "@react-stately/calendar": "3.4.1", + "@react-stately/utils": "^3.8.0", + "@react-types/button": "^3.9.0", + "@react-types/calendar": "3.4.1", + "@react-types/shared": "3.21.0", + "@types/lodash.debounce": "^4.0.7", + "lodash.debounce": "^4.0.8", + "scroll-into-view-if-needed": "3.0.10" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/calendar/node_modules/@react-types/shared": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.21.0.tgz", + "integrity": "sha512-wJA2cUF8dP4LkuNUt9Vh2kkfiQb2NLnV2pPXxVnKJZ7d4x2/7VPccN+LYPnH8m0X3+rt50cxWuPKQmjxSsCFOg==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@nextui-org/divider": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@nextui-org/divider/-/divider-2.0.10.tgz", - "integrity": "sha512-5IuKtkDRX5jAVuxWyIP6BvuJuzUuyOL5V50siWqPgEqtt0O+iuDhSFuioDDWT4gv10CxvKfApm6Dx3+uFU99ZA==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/card": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@nextui-org/card/-/card-2.0.28.tgz", + "integrity": "sha512-Vwa7Poi1kxqjnTWQS9FAGlQw301RqkMlY5cnYQCGeKNbFX+y6u1MlqTSi8ed6RqmdjO23j1zG2+XlBieFyJ9Mg==", "dependencies": { - "@nextui-org/react-rsc-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system-rsc": "2.0.3", - "@nextui-org/theme": "2.0.5", - "@react-types/shared": "^3.19.0" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/ripple": "2.0.28", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-aria-button": "2.0.7", + "@react-aria/button": "^3.9.3", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/checkbox": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@nextui-org/checkbox/-/checkbox-2.0.29.tgz", + "integrity": "sha512-Ed1ahtrFoewt61TPi3aDFZAeA2+Dn+D4A798A2OPBPMHLe70xBPL84Vi35okeY3bzUdBwWQKLMGXbz9nM26sZA==", + "dependencies": { + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-callback-ref": "2.0.5", + "@nextui-org/use-safe-layout-effect": "2.0.5", + "@react-aria/checkbox": "^3.14.1", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-aria/visually-hidden": "^3.8.10", + "@react-stately/checkbox": "^3.6.3", + "@react-stately/toggle": "^3.7.2", + "@react-types/checkbox": "^3.7.1", + "@react-types/shared": "^3.22.1" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/dropdown": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@nextui-org/dropdown/-/dropdown-2.0.13.tgz", - "integrity": "sha512-WzBX0BW/kSV5phfFlPJR0Ia2kERi0jgJvN5+vAol9iSNlBdbYkJnKJJWNeb1+Zys5G5M+697XQvwQKtWkFA7yQ==", - "dependencies": { - "@nextui-org/aria-utils": "2.0.5", - "@nextui-org/divider": "2.0.10", - "@nextui-org/popover": "2.0.12", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-is-mobile": "2.0.3", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/menu": "^3.10.1", - "@react-aria/utils": "^3.19.0", - "@react-stately/menu": "^3.5.4", - "@react-stately/tree": "^3.7.1", - "@react-types/menu": "^3.9.3", - "@react-types/shared": "^3.19.0" + "node_modules/@nextui-org/react/node_modules/@nextui-org/chip": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@nextui-org/chip/-/chip-2.0.28.tgz", + "integrity": "sha512-oD28KZx+PuaWkHlizvMgOAxIkL9cblwun0IhqEztKcR2DMRVdH/4r8/Zdo6QQFDhXlUU0Ub5+WUOyHndwNj0pg==", + "dependencies": { + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-types/checkbox": "^3.7.1" }, "peerDependencies": { - "framer-motion": ">=4.0.0", - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/framer-transitions": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nextui-org/framer-transitions/-/framer-transitions-2.0.5.tgz", - "integrity": "sha512-lf4u6cQZyCgxwX2HLYqos3czbt3174W6cyptxDSHW1VxZYlRhm4II31awR9ZGxaU0d4I2v5PJxNqj70oSNz2rg==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/code": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/code/-/code-2.0.27.tgz", + "integrity": "sha512-gDK48LMNSgQIeUs5WZ53s/hRqDfTMuDdDNgQcmt0bRWMlUC2BTuBfQGzK4y9wbJA9mlWocia7ZDWRWyJrB4vjQ==", "dependencies": { - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/system-rsc": "2.1.1" }, "peerDependencies": { - "framer-motion": ">=4.0.0", - "react": ">=18" + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/date-input": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nextui-org/date-input/-/date-input-2.0.3.tgz", + "integrity": "sha512-7WMJGptHHl+P0LpKk3a7e/Dj86Np66RGLVzWWlFipe7hrg+wJCdkuWCyj6V9mNgH/sdkVKhfkGYT2MogNbOhdA==", + "dependencies": { + "@internationalized/date": "^3.5.2", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@react-aria/datepicker": "^3.9.3", + "@react-aria/i18n": "^3.8.4", + "@react-aria/utils": "^3.21.1", + "@react-stately/datepicker": "^3.9.2", + "@react-types/datepicker": "^3.7.2", + "@react-types/shared": "3.21.0" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/date-input/node_modules/@react-types/shared": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.21.0.tgz", + "integrity": "sha512-wJA2cUF8dP4LkuNUt9Vh2kkfiQb2NLnV2pPXxVnKJZ7d4x2/7VPccN+LYPnH8m0X3+rt50cxWuPKQmjxSsCFOg==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/date-picker": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@nextui-org/date-picker/-/date-picker-2.0.7.tgz", + "integrity": "sha512-03Jys6JMthgX1BMW9R1MKPkHkoetXf4bYZRETAXU5Y9cY1TcosY0FiDEwAUCjlusYOq2UWMRYH4q83tCmir6ag==", + "dependencies": { + "@internationalized/date": "^3.5.2", + "@nextui-org/button": "2.0.31", + "@nextui-org/calendar": "2.0.4", + "@nextui-org/date-input": "2.0.3", + "@nextui-org/popover": "2.1.21", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@react-aria/datepicker": "^3.9.3", + "@react-aria/i18n": "^3.8.4", + "@react-aria/utils": "^3.21.1", + "@react-stately/datepicker": "^3.9.2", + "@react-stately/overlays": "^3.6.3", + "@react-stately/utils": "^3.8.0", + "@react-types/datepicker": "^3.7.2", + "@react-types/shared": "3.21.0" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/date-picker/node_modules/@react-types/shared": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.21.0.tgz", + "integrity": "sha512-wJA2cUF8dP4LkuNUt9Vh2kkfiQb2NLnV2pPXxVnKJZ7d4x2/7VPccN+LYPnH8m0X3+rt50cxWuPKQmjxSsCFOg==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@nextui-org/image": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/image/-/image-2.0.11.tgz", - "integrity": "sha512-G8pRvcfOWPKdnLx3nqhDFT9N0VS2TrBZA97m9pFkmiD/cqGDc3mVv/s/sS/yg/qX1EFY9dNmP9hj+bQdZKvkHg==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/divider": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/divider/-/divider-2.0.27.tgz", + "integrity": "sha512-530oEHonzaxKxspoaKnBFJ4InGqXv2FgOYzEPAMWoMmLb4/zp7e5lRipFKqRsN+zdwIkRNH6c0VJmHfyWI+bUg==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-image": "2.0.2" + "@nextui-org/react-rsc-utils": "2.0.12", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/system-rsc": "2.1.1", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/input": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/@nextui-org/input/-/input-2.0.14.tgz", - "integrity": "sha512-r6ND9JbhOlynbcEOpa0ZH1qYWEvp20+MtTJ8msofIO6LV2ibCoqPMlgepk4ced7UPlashW4lzZa7r38Agb4lEQ==", - "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-icons": "2.0.2", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/textfield": "^3.11.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/utils": "^3.7.0", - "@react-types/shared": "^3.19.0", - "@react-types/textfield": "^3.7.3", - "react-textarea-autosize": "^8.5.2" + "node_modules/@nextui-org/react/node_modules/@nextui-org/dropdown": { + "version": "2.1.23", + "resolved": "https://registry.npmjs.org/@nextui-org/dropdown/-/dropdown-2.1.23.tgz", + "integrity": "sha512-4wAzUbKztvuzzuJcLuDKhvnxB++EQ2aATbCdnfcBA5IyBxj6k4lbalgmSQxtx6D4dm5iJeiOWCJHRZgsIqkxRg==", + "dependencies": { + "@nextui-org/menu": "2.0.22", + "@nextui-org/popover": "2.1.21", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/menu": "^3.13.1", + "@react-aria/utils": "^3.23.2", + "@react-stately/menu": "^3.6.1", + "@react-types/menu": "^3.9.7" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/kbd": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@nextui-org/kbd/-/kbd-2.0.10.tgz", - "integrity": "sha512-0zvJzLfdBZCOa48Wbjt4+MTklDVBILMCxHB4H7Sh8roPwIuqAfvnOi1xQYa+0AXoTT6J5Z7YL1ZK1EZkYCZKXg==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/image": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/image/-/image-2.0.27.tgz", + "integrity": "sha512-EJa1bsZL8zsnTOVd+ZY04ldBz177CO/igz16rpRjo1KPMDX0fxlcjUbUopMfujIASytA68Yq4U1rxfO/xJthuQ==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system-rsc": "2.0.3", - "@nextui-org/theme": "2.0.5", - "@react-aria/utils": "^3.19.0" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-image": "2.0.5" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/input": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/@nextui-org/input/-/input-2.1.21.tgz", + "integrity": "sha512-jwTD4RnpTuieSuLOYqW7Dw2To6E9OVJtcyRBYNIT6GaejT3YG4qaST7BMKz0pJW6mgF9M+pDeKcdOvOqEbOoDg==", + "dependencies": { + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-safe-layout-effect": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/textfield": "^3.14.3", + "@react-aria/utils": "^3.23.2", + "@react-stately/utils": "^3.9.1", + "@react-types/shared": "^3.22.1", + "@react-types/textfield": "^3.9.1", + "react-textarea-autosize": "^8.5.3" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/link": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/link/-/link-2.0.11.tgz", - "integrity": "sha512-gDnmSn9OsoNb2bxJjVSHKgfHFqweZc2uOSB6hde64xlw9m3LbPCSlC8N414+TTxWhfrnyQijkwx5KxsUnFoxOg==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/kbd": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@nextui-org/kbd/-/kbd-2.0.28.tgz", + "integrity": "sha512-raH2Nw+wAHO54swTduLLs/Vdg2/mbMHEe0Y7ud6D13lPexWHVfxUzt7C39/9y8nKh0SpgOkcWV+EmQBydLAI7A==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-icons": "2.0.2", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@react-aria/focus": "^3.14.0", - "@react-aria/link": "^3.5.3", - "@react-aria/utils": "^3.19.0", - "@react-types/link": "^3.4.4" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/system-rsc": "2.1.1", + "@react-aria/utils": "^3.23.2" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/modal": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@nextui-org/modal/-/modal-2.0.13.tgz", - "integrity": "sha512-SJbwN/e3swKE3Kx0/BBWHSPdP6NbtrwqH4GhY0ulpIQ0ZCziy8MAT76KIN46Fh/kBTzsRIXtPcAaf2bXEX3T1Q==", - "dependencies": { - "@nextui-org/framer-transitions": "2.0.5", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-icons": "2.0.2", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-aria-button": "2.0.3", - "@nextui-org/use-aria-modal-overlay": "2.0.3", - "@nextui-org/use-disclosure": "2.0.3", - "@react-aria/dialog": "^3.5.4", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/overlays": "^3.16.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/overlays": "^3.6.1", - "@react-types/overlays": "^3.8.1", - "react-remove-scroll": "^2.5.6" + "node_modules/@nextui-org/react/node_modules/@nextui-org/link": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@nextui-org/link/-/link-2.0.29.tgz", + "integrity": "sha512-OfOi7GLj3apimwAsAXTRZ8/B0tWvx/yXLZFtEe9676+tlLND1nfmWyBHdDIx5WMMiLc3Q1M3FkNrZvigeKQIbQ==", + "dependencies": { + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-aria-link": "2.0.16", + "@react-aria/focus": "^3.16.2", + "@react-aria/link": "^3.6.5", + "@react-aria/utils": "^3.23.2", + "@react-types/link": "^3.5.3" }, "peerDependencies": { - "framer-motion": ">=4.0.0", - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/listbox": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/@nextui-org/listbox/-/listbox-2.1.19.tgz", + "integrity": "sha512-9qQs9KwdDHZ3VaSz4SkYcqn8onuSMCiZElta1vyqJGMWW6JYjJ4DtUOiyqwJdzZOQLIlxazT+GCWjjFUZwFZlQ==", + "dependencies": { + "@nextui-org/aria-utils": "2.0.18", + "@nextui-org/divider": "2.0.27", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-is-mobile": "2.0.7", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/listbox": "^3.11.5", + "@react-aria/utils": "^3.23.2", + "@react-stately/list": "^3.10.3", + "@react-types/menu": "^3.9.7", + "@react-types/shared": "^3.22.1" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/navbar": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/navbar/-/navbar-2.0.11.tgz", - "integrity": "sha512-y+GsQzJDOEvSbGXQb+cFR083mcjwI0VNwbPDMX+16euVL1gqIXqkrQiEiTxD6+urztMu41hOhSqMEb8ratbt0w==", - "dependencies": { - "@nextui-org/framer-transitions": "2.0.5", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-aria-toggle-button": "2.0.3", - "@nextui-org/use-scroll-position": "2.0.2", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/overlays": "^3.16.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/toggle": "^3.6.1", - "@react-stately/utils": "^3.7.0", + "node_modules/@nextui-org/react/node_modules/@nextui-org/menu": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/@nextui-org/menu/-/menu-2.0.22.tgz", + "integrity": "sha512-zU1MbyDPk0QNAVZUSDJSMmdVxpFzWHyiLqOtS+b+kZLdn0va+QBR6LPj237PhyQueChNyz/y8eDDbJ0D6bWf/g==", + "dependencies": { + "@nextui-org/aria-utils": "2.0.18", + "@nextui-org/divider": "2.0.27", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-aria-menu": "2.0.2", + "@nextui-org/use-is-mobile": "2.0.7", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/menu": "^3.13.1", + "@react-aria/utils": "^3.23.2", + "@react-stately/menu": "^3.6.1", + "@react-stately/tree": "^3.7.6", + "@react-types/menu": "^3.9.7", + "@react-types/shared": "^3.22.1" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/modal": { + "version": "2.0.33", + "resolved": "https://registry.npmjs.org/@nextui-org/modal/-/modal-2.0.33.tgz", + "integrity": "sha512-YCgWUMNiVMXAgd6SmU4yH7Ifrz+cmtlF2sK9DBL8kaIZtqAjuhPQj0uQnetvXpY649vomJWVdh9QYHNfD1Jv1Q==", + "dependencies": { + "@nextui-org/framer-utils": "2.0.18", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-aria-button": "2.0.7", + "@nextui-org/use-aria-modal-overlay": "2.0.8", + "@nextui-org/use-disclosure": "2.0.7", + "@react-aria/dialog": "^3.5.12", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/overlays": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-stately/overlays": "^3.6.5", + "@react-types/overlays": "^3.8.5" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/navbar": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/@nextui-org/navbar/-/navbar-2.0.30.tgz", + "integrity": "sha512-Iaw3BU0gdX14nBtZUUFRnsXodnCe1Sbsv9Xk7OI44p+KbOhySgfcjf4iFcXM0vfTOMlOkBSsUzR9bt+/69G5pw==", + "dependencies": { + "@nextui-org/framer-utils": "2.0.18", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-aria-toggle-button": "2.0.7", + "@nextui-org/use-scroll-position": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/overlays": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-stately/toggle": "^3.7.2", + "@react-stately/utils": "^3.9.1", "react-remove-scroll": "^2.5.6" }, "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", "framer-motion": ">=4.0.0", - "react": ">=18" - } - }, - "node_modules/@nextui-org/pagination": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@nextui-org/pagination/-/pagination-2.0.12.tgz", - "integrity": "sha512-IQcHfY8hdYWIzLhWuxcMWUBgDlzqfVSynfVfk5YAbskoGYVG6Bai47hs1isxG99mqiC6R0Bi37KahkyRphgpsg==", - "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-icons": "2.0.2", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-pagination": "2.0.2", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/pagination": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/@nextui-org/pagination/-/pagination-2.0.30.tgz", + "integrity": "sha512-tdlSbNTpqr+aww8h9+7d2Iu0ZX6GGtREeVAbf2+jr5j7VF/VVMVm2eaLJ4m1vw7VQIrEMwKNrcP8QCMMT0a+SQ==", + "dependencies": { + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-pagination": "2.0.6", + "@react-aria/focus": "^3.16.2", + "@react-aria/i18n": "^3.10.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", "scroll-into-view-if-needed": "3.0.10" }, "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@nextui-org/popover": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@nextui-org/popover/-/popover-2.0.12.tgz", - "integrity": "sha512-90XxnaZN97IsFLUQhIKM0Yog8GMRjNC55KXVr3zN0iw7W+RleFrMr52n+nbcuzUshKkSlge4Dxjh4An9zBFOvA==", - "dependencies": { - "@nextui-org/aria-utils": "2.0.5", - "@nextui-org/button": "2.0.11", - "@nextui-org/framer-transitions": "2.0.5", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-aria-button": "2.0.3", - "@react-aria/dialog": "^3.5.4", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/overlays": "^3.16.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/overlays": "^3.6.1", - "@react-types/button": "^3.7.4", - "@react-types/overlays": "^3.8.1", + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/popover": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/@nextui-org/popover/-/popover-2.1.21.tgz", + "integrity": "sha512-Loa6eoAYW0DacDIW+/SC//0LhDDAMnUcd8R9axXtKd00N0Zgnj3YpUJoyLRYvwl5I/FWwV1nCOAvndzW6JJvpQ==", + "dependencies": { + "@nextui-org/aria-utils": "2.0.18", + "@nextui-org/button": "2.0.31", + "@nextui-org/framer-utils": "2.0.18", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-aria-button": "2.0.7", + "@nextui-org/use-safe-layout-effect": "2.0.5", + "@react-aria/dialog": "^3.5.12", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/overlays": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-stately/overlays": "^3.6.5", + "@react-types/button": "^3.9.2", + "@react-types/overlays": "^3.8.5", "react-remove-scroll": "^2.5.6" }, "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", "framer-motion": ">=4.0.0", - "react": ">=18" + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/progress": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/progress/-/progress-2.0.11.tgz", - "integrity": "sha512-RFP7fTFPwXt7b7CSzmDKTLWbq/zx0rKbolfem6wYJFkO0F8S4UrUrTGTwSW06s1mBki35Gb1qMzE//apE51wiw==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/progress": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@nextui-org/progress/-/progress-2.0.28.tgz", + "integrity": "sha512-3Wp6mUeKzw0onLB7/JR1HI3+Y4zf0immVnQp3TYr2zvM5PLAy6RXKtACEGkJanBPfvx4tv3YAIF3419WMvmniw==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-is-mounted": "2.0.2", - "@react-aria/i18n": "^3.8.1", - "@react-aria/progress": "^3.4.4", - "@react-aria/utils": "^3.19.0", - "@react-types/progress": "^3.4.1" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-is-mounted": "2.0.5", + "@react-aria/i18n": "^3.10.2", + "@react-aria/progress": "^3.4.11", + "@react-aria/utils": "^3.23.2", + "@react-types/progress": "^3.5.2" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/radio": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@nextui-org/radio/-/radio-2.0.12.tgz", - "integrity": "sha512-wPFBWBbIhTMg7dc6sMdmBdjVo+cCIIplrsxTruv1cXp+JGv63UpVNfbsCriiMn4yMnQNkiwRKtz4ypQN2k4skw==", - "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/radio": "^3.7.0", - "@react-aria/utils": "^3.19.0", - "@react-aria/visually-hidden": "^3.8.3", - "@react-stately/radio": "^3.8.3", - "@react-types/radio": "^3.5.0", - "@react-types/shared": "^3.19.0" + "node_modules/@nextui-org/react/node_modules/@nextui-org/radio": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@nextui-org/radio/-/radio-2.0.28.tgz", + "integrity": "sha512-h8SSQTDj0NzB13r77RrcEDuWNSpE00ioO7GJKTROd09YQSmck/AID1+ktsDMRQYjoPMPJ7vgwJHuRoKIjXn1CQ==", + "dependencies": { + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/radio": "^3.10.2", + "@react-aria/utils": "^3.23.2", + "@react-aria/visually-hidden": "^3.8.10", + "@react-stately/radio": "^3.10.2", + "@react-types/radio": "^3.7.1", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/react": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/@nextui-org/react/-/react-2.0.22.tgz", - "integrity": "sha512-2+614OeBPq8p7tVRi9onWkBdBUzPRXOsdJf1hpKXOhmDsm4uhbshsx6moty9NyknWWdS8KPoLQOAuFISbtAVzw==", - "dependencies": { - "@nextui-org/accordion": "2.0.12", - "@nextui-org/avatar": "2.0.11", - "@nextui-org/badge": "2.0.9", - "@nextui-org/button": "2.0.11", - "@nextui-org/card": "2.0.11", - "@nextui-org/checkbox": "2.0.12", - "@nextui-org/chip": "2.0.11", - "@nextui-org/code": "2.0.9", - "@nextui-org/divider": "2.0.10", - "@nextui-org/dropdown": "2.0.13", - "@nextui-org/image": "2.0.11", - "@nextui-org/input": "2.0.14", - "@nextui-org/kbd": "2.0.10", - "@nextui-org/link": "2.0.11", - "@nextui-org/modal": "2.0.13", - "@nextui-org/navbar": "2.0.11", - "@nextui-org/pagination": "2.0.12", - "@nextui-org/popover": "2.0.12", - "@nextui-org/progress": "2.0.11", - "@nextui-org/radio": "2.0.12", - "@nextui-org/skeleton": "2.0.9", - "@nextui-org/snippet": "2.0.14", - "@nextui-org/spacer": "2.0.9", - "@nextui-org/spinner": "2.0.9", - "@nextui-org/switch": "2.0.11", - "@nextui-org/system": "2.0.5", - "@nextui-org/table": "2.0.13", - "@nextui-org/tabs": "2.0.11", - "@nextui-org/theme": "2.0.5", - "@nextui-org/tooltip": "2.0.13", - "@nextui-org/user": "2.0.12", - "@react-aria/visually-hidden": "^3.8.3" + "node_modules/@nextui-org/react/node_modules/@nextui-org/ripple": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@nextui-org/ripple/-/ripple-2.0.28.tgz", + "integrity": "sha512-tAxuPjVncx6rSzdHqcFGiprlUo7p+tkTf0c9RMC47DtgIG1DLhFVr0z6QkggmLd1Tgwcj4a3Oyj/PAQMDRxswg==", + "dependencies": { + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5" }, "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", "framer-motion": ">=4.0.0", - "react": ">=18" + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/react-rsc-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@nextui-org/react-rsc-utils/-/react-rsc-utils-2.0.6.tgz", - "integrity": "sha512-32hglPfvA77lFO1XbDBf8a/bVthRjFTWWsYAMGWFF4GKhl/WYBzVRaFmIQjimd6FOfLFhPvolWHEukH3kTlyyQ==" - }, - "node_modules/@nextui-org/react-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@nextui-org/react-utils/-/react-utils-2.0.6.tgz", - "integrity": "sha512-9mRDJj1hNNdKTxt9idQc/OVI+3UzvSErRCjN2hfVVi2c5JrR5b0/Zpx7PV0PHy7yfGUn1hyvwK5ZTVkNhxISKQ==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/scroll-shadow": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@nextui-org/scroll-shadow/-/scroll-shadow-2.1.16.tgz", + "integrity": "sha512-QkOHNFQqEdfSj6iAKd4SusZpmyaJcBFCvx4zLLrWCXGS0+0KWvuaq/dOE8PXSPo4vts4TGDQp6qQGhk0BFvttg==", "dependencies": { - "@nextui-org/react-rsc-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-data-scroll-overflow": "2.1.4" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/select": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/@nextui-org/select/-/select-2.1.27.tgz", + "integrity": "sha512-SLEOir+I09y9wA1reIJRefovyR48Pn+L6oMIiZqYCA0ndGnz3K1g2gsSZ6fyCb9obwZvjzFGvIsrYkW0btUzlA==", + "dependencies": { + "@nextui-org/aria-utils": "2.0.18", + "@nextui-org/listbox": "2.1.19", + "@nextui-org/popover": "2.1.21", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/scroll-shadow": "2.1.16", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/spinner": "2.0.28", + "@nextui-org/use-aria-button": "2.0.7", + "@nextui-org/use-aria-multiselect": "2.1.5", + "@nextui-org/use-safe-layout-effect": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/form": "^3.0.3", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-aria/visually-hidden": "^3.8.10", + "@react-types/shared": "^3.22.1" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/ripple": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/ripple/-/ripple-2.0.11.tgz", - "integrity": "sha512-sAby00AtQZu8swzoVTPu+QKNXGgaFolhbSKjDIzyudZ2rJYQoSWhxeD6/HpbbaakeZgNJWbeXJRKlbFhDNAD5Q==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/skeleton": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/skeleton/-/skeleton-2.0.27.tgz", + "integrity": "sha512-AolxdzJ4xCyb7i2DwZ1iQGSaLGUBYh/rorO8llBqsXDpvhBANcFF3DbRO3kQ+EVGr5AEbEeurd3RabC2F6wVDA==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/system-rsc": "2.1.1" }, "peerDependencies": { - "framer-motion": ">=4.0.0", - "react": ">=18" + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/shared-icons": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/shared-icons/-/shared-icons-2.0.2.tgz", - "integrity": "sha512-TtGFRY4ihRg8HuaFmwIx83LgCZ5bf2FOs407v23Bb0RDrum+3qZOeuClC8bcvivWZz/KUz9ZugGJdGYYSkMcbA==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/slider": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@nextui-org/slider/-/slider-2.2.9.tgz", + "integrity": "sha512-y/Oxhl1OkY7amgYpHZwCF4dF6Uop0Pb+k6m6CNCeXIBL3KFT1Hw9yd17NrV05BekA1llfJrVHEvzneBuTTbbbA==", + "dependencies": { + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/tooltip": "2.0.33", + "@react-aria/focus": "^3.16.2", + "@react-aria/i18n": "^3.10.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/slider": "^3.7.6", + "@react-aria/utils": "^3.23.2", + "@react-aria/visually-hidden": "^3.8.10", + "@react-stately/slider": "^3.5.2" + }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/shared-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/shared-utils/-/shared-utils-2.0.2.tgz", - "integrity": "sha512-tqWVoJtxYbd/hd/laHE85GaXP+b3HeE1tXYjnObbwM+JIh4uu2/Do7Av7mzzyXwS7sZvyHxhi3zW12oank2ykA==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/snippet": { + "version": "2.0.35", + "resolved": "https://registry.npmjs.org/@nextui-org/snippet/-/snippet-2.0.35.tgz", + "integrity": "sha512-2GYxzt6ZBqgEn6XYgi+uU8YMPfMPCAORMXiw/Q+QTuoLQPgKFqsjnQKV7FI581Dax61mIMI5QL5WsQ0oG6PtFw==", + "dependencies": { + "@nextui-org/button": "2.0.31", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/tooltip": "2.0.33", + "@nextui-org/use-clipboard": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/utils": "^3.23.2" + }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/skeleton": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@nextui-org/skeleton/-/skeleton-2.0.9.tgz", - "integrity": "sha512-p1zC9dxWBOsPA2x6sEGVbdSld9ah62ByB/3pOEbhU80IgnGARMytSnM+n/R0mviNNwKpxaQ23/UoknbbSwPQAg==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/spacer": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/spacer/-/spacer-2.0.27.tgz", + "integrity": "sha512-2zYe6PR7Mk4xQpzEhAAkZ8fBp75h7XhgSB7u1aiqW2hJzcuD82hn1SLoUacrYJeO/FBO5UJKQmc8LT63JtuzWQ==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system-rsc": "2.0.3", - "@nextui-org/theme": "2.0.5" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/system-rsc": "2.1.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/snippet": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/@nextui-org/snippet/-/snippet-2.0.14.tgz", - "integrity": "sha512-alOhjn7NiqCUA10SviJ6peM3zWJwG+NRbCJqzoeJ2saPrJTIyOxcOcU3/4++djcSGuq6LdUKwVl6RrkzX2e5lA==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/spinner": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@nextui-org/spinner/-/spinner-2.0.28.tgz", + "integrity": "sha512-hlixGubd91KFSHIjwE0/vLmkSOtUwl56uFrsHBred2pqq8/1CAVlN7aINwoUotZRc5W0T7lyEQGvf88t0Dd3CA==", "dependencies": { - "@nextui-org/button": "2.0.11", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-icons": "2.0.2", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/tooltip": "2.0.13", - "@nextui-org/use-clipboard": "2.0.2", - "@react-aria/focus": "^3.14.0", - "@react-aria/utils": "^3.19.0" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/system-rsc": "2.1.1" }, "peerDependencies": { - "framer-motion": ">=4.0.0", - "react": ">=18" + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/spacer": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@nextui-org/spacer/-/spacer-2.0.9.tgz", - "integrity": "sha512-AP0qkZp8hpHWGrisdSxPF3tOnlYMwcyWRJK1P59PQehWE831ukGk/wasY/pOZKdGdMR7EZJ804cOAdMlnfOQxw==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/switch": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@nextui-org/switch/-/switch-2.0.28.tgz", + "integrity": "sha512-cogzyB7Ng95WP/neMBWgOLRkw2GC/qLQoW0gTuuT53lTEnAtatFikNoL30CyA/EZzz7YsUjLH2W+9kBiZLtITQ==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system-rsc": "2.0.3", - "@nextui-org/theme": "2.0.5" + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/switch": "^3.6.2", + "@react-aria/utils": "^3.23.2", + "@react-aria/visually-hidden": "^3.8.10", + "@react-stately/toggle": "^3.7.2", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/spinner": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@nextui-org/spinner/-/spinner-2.0.9.tgz", - "integrity": "sha512-7uIlrlrTJ9lW+ldgp0nkgIFNQPlCsTdbQfNb3Vostsu/x15eWBAzf46Qy4UO8gTYtKzH0TUwN9tZ3ycpKxpHyg==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/system-rsc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nextui-org/system-rsc/-/system-rsc-2.1.1.tgz", + "integrity": "sha512-gkTKNAbTZVl81SVJsaLHp4iqyd956y40UIGUXPeq0pwOGLM0xGWSkLbkNT8WtdPUt3bSD9y0xuKbiV3tpSBGOA==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system-rsc": "2.0.3", - "@nextui-org/theme": "2.0.5" + "clsx": "^1.2.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "tailwind-variants": ">=0.1.13" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/table": { + "version": "2.0.33", + "resolved": "https://registry.npmjs.org/@nextui-org/table/-/table-2.0.33.tgz", + "integrity": "sha512-mUqGGWCoEo5z49s60IrVnBDcSgT8K2T5+x5qqmk30v09B6s5c8dqyL7NAC+pk7BayHqr5xEW42EqMbRKmVvtCw==", + "dependencies": { + "@nextui-org/checkbox": "2.0.29", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-icons": "2.0.7", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/spacer": "2.0.27", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/table": "^3.13.5", + "@react-aria/utils": "^3.23.2", + "@react-aria/visually-hidden": "^3.8.10", + "@react-stately/table": "^3.11.6", + "@react-stately/virtualizer": "^3.6.8", + "@react-types/grid": "^3.2.4", + "@react-types/table": "^3.9.3" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/tabs": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@nextui-org/tabs/-/tabs-2.0.29.tgz", + "integrity": "sha512-RthZ+lNyXQ3CNXMRiQdQMGGsWJurS7ESrhowLRtTiDOPYhnJxAMqrqzI3k8ZgDIBirC/1zEoOdn89oqd2Pa5gw==", + "dependencies": { + "@nextui-org/aria-utils": "2.0.18", + "@nextui-org/framer-utils": "2.0.18", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-is-mounted": "2.0.5", + "@nextui-org/use-update-effect": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/tabs": "^3.8.5", + "@react-aria/utils": "^3.23.2", + "@react-stately/tabs": "^3.6.4", + "@react-types/shared": "^3.22.1", + "@react-types/tabs": "^3.3.5", + "scroll-into-view-if-needed": "3.0.10" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/react/node_modules/@nextui-org/tooltip": { + "version": "2.0.33", + "resolved": "https://registry.npmjs.org/@nextui-org/tooltip/-/tooltip-2.0.33.tgz", + "integrity": "sha512-WUpBuoZ1ya2iD9EI2d/E58BpPrRJQ2NDnpIU6RjwWe/MGqtxf3oJVQZd6kKpgaD8eB6P3OSiFTwTUK7+AoLmDQ==", + "dependencies": { + "@nextui-org/aria-utils": "2.0.18", + "@nextui-org/framer-utils": "2.0.18", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@nextui-org/use-safe-layout-effect": "2.0.5", + "@react-aria/interactions": "^3.21.1", + "@react-aria/overlays": "^3.21.1", + "@react-aria/tooltip": "^3.7.2", + "@react-aria/utils": "^3.23.2", + "@react-stately/tooltip": "^3.4.7", + "@react-types/overlays": "^3.8.5", + "@react-types/tooltip": "^3.4.7" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/switch": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/switch/-/switch-2.0.11.tgz", - "integrity": "sha512-SnZzmSx+FEmkN2H+svJcRdHJTDgTDRSKgXZHyH9vzTnPLP/T45WcWMW4eXzQQ99CE5O+rZDHyzZAuh9nucZaeg==", + "node_modules/@nextui-org/react/node_modules/@nextui-org/user": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@nextui-org/user/-/user-2.0.28.tgz", + "integrity": "sha512-1WaAZSIzgRMaA+2+BACelxIE4YvPN6MFW+I3SvODwn98aju1yU485akxjenc7XM/5CC6TGZDAXiFz2VcEFIcZA==", "dependencies": { - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/switch": "^3.5.3", - "@react-aria/utils": "^3.19.0", - "@react-aria/visually-hidden": "^3.8.3", - "@react-stately/toggle": "^3.6.1", - "@react-types/shared": "^3.19.0" + "@nextui-org/avatar": "2.0.27", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/shared-utils": "2.0.5", + "@react-aria/focus": "^3.16.2", + "@react-aria/utils": "^3.23.2" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/system": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nextui-org/system/-/system-2.0.5.tgz", - "integrity": "sha512-ke02e60ctxnXsxe77TRN6pWVY89aCCi0AXwyQIbgT9lfiVgUGWJa+f6am46ItCzeSkvIQ7j3XJBz717zu+GSRg==", - "dependencies": { - "@nextui-org/system-rsc": "2.0.3", - "@react-aria/i18n": "^3.8.1", - "@react-aria/overlays": "^3.16.0" - }, + "node_modules/@nextui-org/react/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@nextui-org/shared-icons": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@nextui-org/shared-icons/-/shared-icons-2.0.7.tgz", + "integrity": "sha512-GsotFeRbwxhc2eQt7Z6edcVYfklpaSzo93Xodryb82SokRaSOKt9BEpUXgk2TExAvJMjDnB4T8nk8ANWsFaXOw==", "peerDependencies": { "react": ">=18" } }, - "node_modules/@nextui-org/system-rsc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nextui-org/system-rsc/-/system-rsc-2.0.3.tgz", - "integrity": "sha512-CfWl1JkHPtgSvq/Szz4oWQAg7b6L7wxxHEFzKcvNdhbX61IfE5PvnQMl5z8T9UTSIIwFv4P0ntP5kpKE895w2w==", + "node_modules/@nextui-org/shared-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/shared-utils/-/shared-utils-2.0.5.tgz", + "integrity": "sha512-aFc/CUL8RVfBh0IotIpxkpKjyUPc/zJaMJd5pRCQA1kIpKLdSrlh3//MLYMaP/fo/NQtE3DPeXqfKhHRr1fkEw==" + }, + "node_modules/@nextui-org/system": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@nextui-org/system/-/system-2.1.2.tgz", + "integrity": "sha512-dkj2DAye6pjpVheoJKup+L8CyK774YORudkum+5zCuwyOe50IV2j6wbGqyWir9cI1fruFUsfzQ1NR4KljWNqFQ==", "dependencies": { - "clsx": "^1.2.1", - "tailwind-variants": "^0.1.13" + "@internationalized/date": "^3.5.2", + "@nextui-org/react-utils": "2.0.13", + "@nextui-org/system-rsc": "2.1.1", + "@react-aria/i18n": "^3.10.2", + "@react-aria/overlays": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-stately/utils": "^3.9.1" }, "peerDependencies": { - "react": ">=18" + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@nextui-org/table": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@nextui-org/table/-/table-2.0.13.tgz", - "integrity": "sha512-Cw2u/D7Y8M1D1LVZjDFR+6CzYHZENKfClzh67QV3o9P3M15EWXMNnSKHfl1ZCJT3ogKaiNJD6JOB9qC6/wjhBA==", - "dependencies": { - "@nextui-org/checkbox": "2.0.12", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-icons": "2.0.2", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/spacer": "2.0.9", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/table": "^3.11.0", - "@react-aria/utils": "^3.19.0", - "@react-aria/visually-hidden": "^3.8.3", - "@react-stately/table": "^3.11.0", - "@react-stately/virtualizer": "^3.6.0", - "@react-types/grid": "^3.2.0", - "@react-types/table": "^3.8.0" + "node_modules/@nextui-org/system/node_modules/@nextui-org/system-rsc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nextui-org/system-rsc/-/system-rsc-2.1.1.tgz", + "integrity": "sha512-gkTKNAbTZVl81SVJsaLHp4iqyd956y40UIGUXPeq0pwOGLM0xGWSkLbkNT8WtdPUt3bSD9y0xuKbiV3tpSBGOA==", + "dependencies": { + "clsx": "^1.2.1" }, "peerDependencies": { - "react": ">=18" + "@nextui-org/theme": ">=2.1.0", + "react": ">=18", + "tailwind-variants": ">=0.1.13" } }, - "node_modules/@nextui-org/tabs": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nextui-org/tabs/-/tabs-2.0.11.tgz", - "integrity": "sha512-7LsOAuhDF1MscYM1l0v0S1l77cS6CT99C6Mr5VDu9VbFraSiB+I2sFAwlwkTRxYvrX5JVSwywyU3zvwQcJ/e+w==", - "dependencies": { - "@nextui-org/aria-utils": "2.0.5", - "@nextui-org/framer-transitions": "2.0.5", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@nextui-org/use-is-mounted": "2.0.2", - "@nextui-org/use-update-effect": "2.0.2", - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/tabs": "^3.6.2", - "@react-aria/utils": "^3.19.0", - "@react-stately/tabs": "^3.5.1", - "@react-types/shared": "^3.19.0", - "@react-types/tabs": "^3.3.1", - "scroll-into-view-if-needed": "3.0.10" - }, - "peerDependencies": { - "framer-motion": ">=4.0.0", - "react": ">=18" + "node_modules/@nextui-org/system/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" } }, "node_modules/@nextui-org/theme": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.0.5.tgz", - "integrity": "sha512-4br6xRTYGbDCL8Vwm4CP0VqEXORROyQCmLSwNFaXr6s0vB5nX5tnJHqyDIvpq5Al7I2H8HMG2KOUHyhbaKOKFA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.2.3.tgz", + "integrity": "sha512-p8gZ+4dQxA4ZO9RxVljAs37EYtQsw0n9DtXD6f395gpl0DLKRq/d4oCQ887oC6lHDyTibtaHHtOu+MKzK6j7Gw==", "dependencies": { "color": "^4.2.3", "color2k": "^2.0.2", @@ -1602,206 +1960,247 @@ "lodash.kebabcase": "^4.1.1", "lodash.mapkeys": "^4.6.0", "lodash.omit": "^4.5.0", - "tailwind-variants": "^0.1.13", - "tailwindcss": "^3.2.7" + "tailwind-variants": "^0.1.20" }, "peerDependencies": { - "tailwindcss": "*" + "tailwindcss": ">=3.4.0" } }, - "node_modules/@nextui-org/tooltip": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@nextui-org/tooltip/-/tooltip-2.0.13.tgz", - "integrity": "sha512-CY8hn9PEJJmip/q+pL8nxksI/xN0iyzRbOZHEGVZs6tPkny8BM0OhRh0oxmP17zKqz5PhijJ0FSsgC4uSWAiMg==", - "dependencies": { - "@nextui-org/aria-utils": "2.0.5", - "@nextui-org/framer-transitions": "2.0.5", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@react-aria/interactions": "^3.17.0", - "@react-aria/overlays": "^3.16.0", - "@react-aria/tooltip": "^3.6.1", - "@react-aria/utils": "^3.19.0", - "@react-stately/tooltip": "^3.4.3", - "@react-types/overlays": "^3.8.1", - "@react-types/tooltip": "^3.4.3" + "node_modules/@nextui-org/use-aria-accordion": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-accordion/-/use-aria-accordion-2.0.4.tgz", + "integrity": "sha512-5OEi7zrv1F25XCjXSx+tOvyJWN/Modj9+iz5v/QXDJN76sFVIoCoNsUlZS5Vokyt5fImXb3SAlWvOPehqLbSGA==", + "dependencies": { + "@react-aria/button": "^3.9.3", + "@react-aria/focus": "^3.16.2", + "@react-aria/selection": "^3.17.5", + "@react-aria/utils": "^3.23.2", + "@react-stately/tree": "^3.7.6", + "@react-types/accordion": "3.0.0-alpha.19", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { - "framer-motion": ">=4.0.0", "react": ">=18" } }, - "node_modules/@nextui-org/use-aria-accordion-item": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-accordion-item/-/use-aria-accordion-item-2.0.3.tgz", - "integrity": "sha512-nHJypFAwbeKn86KhfOXnf92D89CD5IETuq7RbWm0EC++NajPxRDjJl/zgPef7UWIvq28UC4TRVC7LAknFH7L/Q==", + "node_modules/@nextui-org/use-aria-button": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-button/-/use-aria-button-2.0.7.tgz", + "integrity": "sha512-Cttt4C802RQX6Wae/IiuzdOCVjzHDnUMK8MBwkdDEKR/TVGjaTvPbLOJSw7FNmz0mIrtp7zaTHlRvrbDJmvnIQ==", "dependencies": { - "@react-aria/button": "^3.8.1", - "@react-aria/focus": "^3.14.0", - "@react-stately/tree": "^3.7.1", - "@react-types/shared": "^3.19.0" + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-types/button": "^3.9.2", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { "react": ">=18" } }, - "node_modules/@nextui-org/use-aria-button": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-button/-/use-aria-button-2.0.3.tgz", - "integrity": "sha512-JdxOk12vXO/AVLwJ0Mnr9QTugLDnjOPfDoV/AtQVGxgU/7VAuyGVt2Gt5eXQM6eOm36UBia59eXlWzF/9Judjw==", + "node_modules/@nextui-org/use-aria-link": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-link/-/use-aria-link-2.0.16.tgz", + "integrity": "sha512-nxaSkHlSNbsODYDusoh6+bt8B7ndoAD82pC1b0c0M0kFP14hktzIf9noaY+bSujcI9MlLJR1WLwZoHGYC5Mlng==", "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-types/button": "^3.7.4", - "@react-types/shared": "^3.19.0" + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-types/link": "^3.5.3", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { "react": ">=18" } }, + "node_modules/@nextui-org/use-aria-menu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-menu/-/use-aria-menu-2.0.2.tgz", + "integrity": "sha512-YV/tp246bWTfZIa6eDnN19Z0VkOB5/SP9qlLtigY0a2lPuGQ/6o3LpcWZxQPOgLwBd6PQwUgNe/RakOO3rRrAQ==", + "dependencies": { + "@react-aria/i18n": "^3.10.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/menu": "^3.13.1", + "@react-aria/selection": "^3.17.5", + "@react-aria/utils": "^3.23.2", + "@react-stately/collections": "^3.10.5", + "@react-stately/tree": "^3.7.6", + "@react-types/menu": "^3.9.7", + "@react-types/shared": "^3.22.1" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/@nextui-org/use-aria-modal-overlay": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-modal-overlay/-/use-aria-modal-overlay-2.0.3.tgz", - "integrity": "sha512-ajh7bEV+OaQ7s6DWC3rBwbkr8eTi2/ykf4mNc/682Y5cuR2ZPrBGg00HStVcNDZOdwUBArVhTWQaQ8e+0lwBww==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-modal-overlay/-/use-aria-modal-overlay-2.0.8.tgz", + "integrity": "sha512-fzMh/UtNEzYKOcjXyM1esGoxorB4nBPkg8vyGqVgkhU+QeI0JdWPEnC6nXAU6j57eh3ZYx/jLEMh1Jeu5IAEmw==", "dependencies": { - "@react-aria/overlays": "^3.16.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/overlays": "^3.6.1", - "@react-types/shared": "^3.19.0" + "@react-aria/overlays": "^3.21.1", + "@react-aria/utils": "^3.23.2", + "@react-stately/overlays": "^3.6.5", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { - "react": ">=18" + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@nextui-org/use-aria-multiselect": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-multiselect/-/use-aria-multiselect-2.1.5.tgz", + "integrity": "sha512-AIWVu6iW4EX8RrnNtt3mHxDFtbQ7Io/mr0dpaE/s5HbfPMjljktMdP22YLYUnRXHqOeAfqtRSa9Mq7Qpec2Vtw==", + "dependencies": { + "@react-aria/i18n": "^3.10.2", + "@react-aria/interactions": "^3.21.1", + "@react-aria/label": "^3.7.6", + "@react-aria/listbox": "^3.11.5", + "@react-aria/menu": "^3.13.1", + "@react-aria/selection": "^3.17.5", + "@react-aria/utils": "^3.23.2", + "@react-stately/form": "^3.0.1", + "@react-stately/list": "^3.10.3", + "@react-stately/menu": "^3.6.1", + "@react-types/button": "^3.9.2", + "@react-types/overlays": "^3.8.5", + "@react-types/select": "^3.9.2", + "@react-types/shared": "^3.22.1" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/@nextui-org/use-aria-toggle-button": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-toggle-button/-/use-aria-toggle-button-2.0.3.tgz", - "integrity": "sha512-puoPTmrxx7l9vh42oAnkAc57uF+TldUg3G2A/Gc7aTcIq1AXLfaTDaxWiRel6W0Ew2H/IKu9AlQScY28HOQ/iA==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-toggle-button/-/use-aria-toggle-button-2.0.7.tgz", + "integrity": "sha512-m+1qjSoJrzMf6oefh1RTYSA0l/JbU9v3cHwpoX/OjCE6q3EpLaqgI/U679oxpd7OLPrWq6HmBKOzKt6ZmokMYw==", "dependencies": { - "@nextui-org/use-aria-button": "2.0.3", - "@react-aria/utils": "^3.19.0", - "@react-stately/toggle": "^3.6.1", - "@react-types/button": "^3.7.4", - "@react-types/shared": "^3.19.0" + "@nextui-org/use-aria-button": "2.0.7", + "@react-aria/utils": "^3.23.2", + "@react-stately/toggle": "^3.7.2", + "@react-types/button": "^3.9.2", + "@react-types/shared": "^3.22.1" }, "peerDependencies": { "react": ">=18" } }, "node_modules/@nextui-org/use-callback-ref": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-callback-ref/-/use-callback-ref-2.0.2.tgz", - "integrity": "sha512-avKTXdy/bOfjPKTBj1RIdkbdqTC9ICZUzb5GejR4riA3zCcHwS2JxjQTGb9xNF3Y5DyH1Mb7hf2+jBmqF2g/QA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-callback-ref/-/use-callback-ref-2.0.5.tgz", + "integrity": "sha512-lcjlV5yaDTiFSv06E5RtQNqy+O6XqH/Q/yz+ka1ZBlZF/FdzEPNRfJ0shN2D7Sh3DdbvV2lySbA2g/0d94geaw==", "dependencies": { - "@nextui-org/use-safe-layout-effect": "2.0.2" + "@nextui-org/use-safe-layout-effect": "2.0.5" }, "peerDependencies": { "react": ">=18" } }, "node_modules/@nextui-org/use-clipboard": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-clipboard/-/use-clipboard-2.0.2.tgz", - "integrity": "sha512-Ass+LJR/cWC48AeIUtsukzvA7Mf5bV7ikdNUvuLyrc9pdqr1fmw4aHCkQPQKSjLIHy85KuXDKqrqhVoVLivD4g==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-clipboard/-/use-clipboard-2.0.5.tgz", + "integrity": "sha512-1ExwXM8ENmc/kVDqKoiPGrBP/0B7rZ43iSv2MoWD1Qpc8GHg71Rv7NTIlBDoD/pfUfqkab6x66iKC7AVR8rifA==", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@nextui-org/use-data-scroll-overflow": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nextui-org/use-data-scroll-overflow/-/use-data-scroll-overflow-2.1.4.tgz", + "integrity": "sha512-0YqUAe/b9aZftUQOH7sWqBMJHGLyC2Q/ixFyjq8Q1TijrqEyGESGQ2tm0+FHytI04drV+mnsbf6+q2QIKyqGSg==", + "dependencies": { + "@nextui-org/shared-utils": "2.0.5" + }, "peerDependencies": { "react": ">=18" } }, "node_modules/@nextui-org/use-disclosure": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nextui-org/use-disclosure/-/use-disclosure-2.0.3.tgz", - "integrity": "sha512-bPs4/wXSytiR5xxhlErkxXc2Fk3siQqLK5g/Qo+f2CQopXTaldkbIIlu/0lzd0KBIApX5z0rOr3bnr9Xu1Wn4A==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@nextui-org/use-disclosure/-/use-disclosure-2.0.7.tgz", + "integrity": "sha512-h86z6H/eTQ6RMAYkWBvItgV0uh4UDTbJIa8hvDguzYLyGk5Ji+7HXotCUwKELrK/+QuOtAFYcJ6+Cp8zp7tZuA==", "dependencies": { - "@nextui-org/use-callback-ref": "2.0.2", - "@react-aria/utils": "^3.19.0", - "@react-stately/utils": "^3.7.0" + "@nextui-org/use-callback-ref": "2.0.5", + "@react-aria/utils": "^3.23.2", + "@react-stately/utils": "^3.9.1" }, "peerDependencies": { "react": ">=18" } }, "node_modules/@nextui-org/use-image": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-image/-/use-image-2.0.2.tgz", - "integrity": "sha512-geCUHp2P/2und98/Ka12dyrw78D9F2qG1a8WN/iB0BQWwaEm8km8YH13zlV0GOFHCwlA5gsXqrUvzxPjfZytZQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-image/-/use-image-2.0.5.tgz", + "integrity": "sha512-FAMyvZS9XSNLqHEmU6xykMgwIFJj/V9/JpTiZAQziz2wqMiUONIBpYpGOlI+pPBNlhCkw62KHm/19vHW49FWhA==", "dependencies": { - "@nextui-org/use-safe-layout-effect": "2.0.2" + "@nextui-org/use-safe-layout-effect": "2.0.5" }, "peerDependencies": { "react": ">=18" } }, "node_modules/@nextui-org/use-is-mobile": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nextui-org/use-is-mobile/-/use-is-mobile-2.0.3.tgz", - "integrity": "sha512-JOxomIBoMIj7CnLVNrnv3wlUQ/3cr3l1OJw947qRzMlN19Q6X8k4bDvuPPlQbY6KL+emB0RM+lsdHv4WQ+Hpdg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@nextui-org/use-is-mobile/-/use-is-mobile-2.0.7.tgz", + "integrity": "sha512-BmOseC8Xmp5Xl8EKrsl/MoYtz0aIkezMatYGBCoGDGUosaKx8kNYv6T2WVA3uKj1Gr3s4dHhMCuISvcpE9XOiQ==", "dependencies": { - "@react-aria/ssr": "^3.7.1" + "@react-aria/ssr": "^3.9.2" }, "peerDependencies": { "react": ">=18" } }, "node_modules/@nextui-org/use-is-mounted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-is-mounted/-/use-is-mounted-2.0.2.tgz", - "integrity": "sha512-PjwpTkl5f+bTVU9l5GzgZDHd+uOwCZ3bhuYzbbamw1J5kBWruVnKUqZihS3zrLtJxKNxk/f7RT0UWK2a4wGpDw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-is-mounted/-/use-is-mounted-2.0.5.tgz", + "integrity": "sha512-gk698Uwmj/XhchBsnI5Ups5uzEXuZvsPK45K6goi2/ADKXSYxHOcSgwoexytqJBb/7tpi+emi2CRTAjAFZDQqA==", "peerDependencies": { - "react": ">=18", - "react-dom": ">=16.8.0" + "react": ">=18" + } + }, + "node_modules/@nextui-org/use-measure": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nextui-org/use-measure/-/use-measure-2.0.1.tgz", + "integrity": "sha512-uEtdrdBdFz4Fgbfk2vmQ+rEb+eFa5o4yI90udasvfpaIrMBfrFOlRW5+yn3uXKB8JThET4Gf2on/wlJpo567Dg==", + "peerDependencies": { + "react": ">=18" } }, "node_modules/@nextui-org/use-pagination": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-pagination/-/use-pagination-2.0.2.tgz", - "integrity": "sha512-wQAmKMXzb0DhhXHx3K/LppaP2n5ZknjOYQpm+TAjOaIPJYIbyNIRa2FFAP/lf8vZCHjHB7+KUVLhkIwAzrZ0dw==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nextui-org/use-pagination/-/use-pagination-2.0.6.tgz", + "integrity": "sha512-/EIrpC/q6xQNDQrODivC3VVkphVmExiFjqqXdyxOHWnhfgC1BhQOqGK0qIPvDoHmk1U7ULKnlh/VuYjGtfTJgg==", "dependencies": { - "@nextui-org/shared-utils": "2.0.2" + "@nextui-org/shared-utils": "2.0.5", + "@react-aria/i18n": "^3.10.2" }, "peerDependencies": { "react": ">=18" } }, "node_modules/@nextui-org/use-safe-layout-effect": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-safe-layout-effect/-/use-safe-layout-effect-2.0.2.tgz", - "integrity": "sha512-HsFP2e+o2eSiQyAXdiicPBj6qj1naHuiNqqeTPqeJBsr0aUZI8l+7vZ5OXjLc8Qou4AOyNyJBBGFNhwsraxdpw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-safe-layout-effect/-/use-safe-layout-effect-2.0.5.tgz", + "integrity": "sha512-YQQlqz82aYxMoEq23jQNG/JBPHF1x3opzyXRHAVxgBEFo9OJqBMZTm23ukpTXm2Ev98T6mpWiTHdfyHJ7IoRog==", "peerDependencies": { "react": ">=18" } }, "node_modules/@nextui-org/use-scroll-position": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-scroll-position/-/use-scroll-position-2.0.2.tgz", - "integrity": "sha512-DHmGMoLrjyuE/YQk92OGxF/v3cLaiBIvDpTxAAMtgerVkkPyuL7O9j9cyLiRz9ad92pL9TJwmjJ/00wJ2Qr/Wg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-scroll-position/-/use-scroll-position-2.0.5.tgz", + "integrity": "sha512-SSHEmv51rXWF4pfQ3YjJuEmUmHFZBLRSM2jtVSfghR3pjckMykFtlyxGhTAcXKAwi5I7rTHcVL2HFOKWSZBdaQ==", "peerDependencies": { "react": ">=18" } }, "node_modules/@nextui-org/use-update-effect": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-update-effect/-/use-update-effect-2.0.2.tgz", - "integrity": "sha512-yN2LWvG2QNDz6XDjRZq6jBQ7+Jaz2eihy+Q7IR+XLXi6fsyKQuYKxphw5VANa0ZbvKVuN/n5m5WRDRmWmeeOWw==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@nextui-org/user": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@nextui-org/user/-/user-2.0.12.tgz", - "integrity": "sha512-AIGZbPX/HPy4oQ+mgyk/jRBd6WazwKDz3M3X0e973B0QPfr2Znw0CaeMMsH9fqLQUCNuAl3pthPKPMI6rVyiyA==", - "dependencies": { - "@nextui-org/avatar": "2.0.11", - "@nextui-org/react-utils": "2.0.6", - "@nextui-org/shared-utils": "2.0.2", - "@nextui-org/system": "2.0.5", - "@nextui-org/theme": "2.0.5", - "@react-aria/focus": "^3.14.0", - "@react-aria/utils": "^3.19.0" - }, + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-update-effect/-/use-update-effect-2.0.5.tgz", + "integrity": "sha512-4r2CXAD598xc2ifMu97kf8V/lj+NDct2oITbxgXeV4ezWaXHy5/26r1iyVnBzRN/VBz3fwHx3hHdftzcYSZxdA==", "peerDependencies": { "react": ">=18" } @@ -1846,6 +2245,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -1855,19 +2263,16 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@react-aria/accordion": { - "version": "3.0.0-alpha.20", - "resolved": "https://registry.npmjs.org/@react-aria/accordion/-/accordion-3.0.0-alpha.20.tgz", - "integrity": "sha512-dQIrZrUwfVIezny/7SknsxIeZ5R4VXMizuCC6XCTDgeu7Mx8O3/+quJwE58KAHT9mhvWx7Wk+QGNBOTNbwSXQQ==", + "node_modules/@react-aria/breadcrumbs": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.12.tgz", + "integrity": "sha512-UHTVe6kA73xbE1J6LLVjUooEQvTJ4vWPRyOxu4t3dZ/4dMttvHxpKylvj4z606wioSGVhCDEKC4Vn+RtQLypeA==", "dependencies": { - "@react-aria/button": "^3.8.1", - "@react-aria/interactions": "^3.17.0", - "@react-aria/selection": "^3.16.1", - "@react-aria/utils": "^3.19.0", - "@react-stately/tree": "^3.7.1", - "@react-types/accordion": "3.0.0-alpha.15", - "@react-types/button": "^3.7.4", - "@react-types/shared": "^3.19.0", + "@react-aria/i18n": "^3.11.0", + "@react-aria/link": "^3.7.0", + "@react-aria/utils": "^3.24.0", + "@react-types/breadcrumbs": "^3.7.4", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -1875,90 +2280,196 @@ } }, "node_modules/@react-aria/button": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.8.1.tgz", - "integrity": "sha512-igxZ871An3Clpmpw+beN8F792NfEnEaLRAZ4jITtC/FdzwQwRM7eCu/ZEaqpNtbUtruAmYhafnG/2uCkKhTpTw==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/toggle": "^3.6.1", - "@react-types/button": "^3.7.4", - "@react-types/shared": "^3.19.0", + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.9.4.tgz", + "integrity": "sha512-YOt4XWtC+0m7LwLQnU1Gl0ENETLEhtM8SyDbwsFR/fIQYX0T0H9D6jMlnKxXDjKgRvHzom9NZ8caTfsEPbgW/g==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/utils": "^3.24.0", + "@react-stately/toggle": "^3.7.3", + "@react-types/button": "^3.9.3", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@react-aria/calendar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@react-aria/calendar/-/calendar-3.5.1.tgz", + "integrity": "sha512-3gGiI2arrGQtlPD9633l00TR4y5dj9IMFapEiCDuwVwNSCsnH8aiz/emg+3hGFq86QoyvkFBvnKmezJIVKfPkA==", + "dependencies": { + "@internationalized/date": "^3.5.0", + "@react-aria/i18n": "^3.8.3", + "@react-aria/interactions": "^3.19.0", + "@react-aria/live-announcer": "^3.3.1", + "@react-aria/utils": "^3.21.0", + "@react-stately/calendar": "^3.4.1", + "@react-types/button": "^3.9.0", + "@react-types/calendar": "^3.4.1", + "@react-types/shared": "^3.21.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, "node_modules/@react-aria/checkbox": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.14.2.tgz", + "integrity": "sha512-PeXTEfURrZZBN80YJUyVPAvkT7gwpPtwBgtKxg1ars+D1iDV4Yp48yh5pKaNSf0/rlLNOgKJSCpcFzY7V3ipFw==", + "dependencies": { + "@react-aria/form": "^3.0.4", + "@react-aria/interactions": "^3.21.2", + "@react-aria/label": "^3.7.7", + "@react-aria/toggle": "^3.10.3", + "@react-aria/utils": "^3.24.0", + "@react-stately/checkbox": "^3.6.4", + "@react-stately/form": "^3.0.2", + "@react-stately/toggle": "^3.7.3", + "@react-types/checkbox": "^3.8.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/combobox": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.9.0.tgz", + "integrity": "sha512-JRiCoARx95Lu1hENmf4ndHzpJrMeP/2bV96jZbMn4StFUzhACKnUw0rNFpFdONfeoD/MkWO7tsvhxaPGLhpgtQ==", + "dependencies": { + "@react-aria/i18n": "^3.11.0", + "@react-aria/listbox": "^3.12.0", + "@react-aria/live-announcer": "^3.3.3", + "@react-aria/menu": "^3.14.0", + "@react-aria/overlays": "^3.22.0", + "@react-aria/selection": "^3.18.0", + "@react-aria/textfield": "^3.14.4", + "@react-aria/utils": "^3.24.0", + "@react-stately/collections": "^3.10.6", + "@react-stately/combobox": "^3.8.3", + "@react-stately/form": "^3.0.2", + "@react-types/button": "^3.9.3", + "@react-types/combobox": "^3.11.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/datepicker": { "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.10.0.tgz", - "integrity": "sha512-1s5jkmag+41Fa2BwoOoM5cRRadDh3N8khgsziuGzD0NqvZLRCtHgDetNlileezFHwOeOWK6zCqDOrYLJhcMi8g==", - "dependencies": { - "@react-aria/label": "^3.6.1", - "@react-aria/toggle": "^3.7.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/checkbox": "^3.4.4", - "@react-stately/toggle": "^3.6.1", - "@react-types/checkbox": "^3.5.0", - "@react-types/shared": "^3.19.0", + "resolved": "https://registry.npmjs.org/@react-aria/datepicker/-/datepicker-3.10.0.tgz", + "integrity": "sha512-YiIxY+mRxc2rPN8j9ypdiGspRHSIrsK6TShBgKEk5UoG5EBKEJfNe/FfoXDR2d5xcpWLAHVuRjERi9WkiJNDBw==", + "dependencies": { + "@internationalized/date": "^3.5.3", + "@internationalized/number": "^3.5.2", + "@internationalized/string": "^3.2.2", + "@react-aria/focus": "^3.17.0", + "@react-aria/form": "^3.0.4", + "@react-aria/i18n": "^3.11.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/label": "^3.7.7", + "@react-aria/spinbutton": "^3.6.4", + "@react-aria/utils": "^3.24.0", + "@react-stately/datepicker": "^3.9.3", + "@react-stately/form": "^3.0.2", + "@react-types/button": "^3.9.3", + "@react-types/calendar": "^3.4.5", + "@react-types/datepicker": "^3.7.3", + "@react-types/dialog": "^3.5.9", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/datepicker/node_modules/@react-types/calendar": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.4.5.tgz", + "integrity": "sha512-FAAUbqe8iPiNf/OtdxnpOuAEJzyeRgfK2QCzfb4BIVnNNaTDkbxGCI5wrqHfBQ4FASECJeNlkjYXtbvijaooyw==", + "dependencies": { + "@internationalized/date": "^3.5.3", + "@react-types/shared": "^3.23.0" + }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-aria/dialog": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.4.tgz", - "integrity": "sha512-+YGjX5ygYvFvnRGDy7LVTL2uRCH5VYosMNKn0vyel99SiwHH9d8fdnnJjVvSJ3u8kvoXk22+OnRE2/vEX+G1EA==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/overlays": "^3.16.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/overlays": "^3.6.1", - "@react-types/dialog": "^3.5.4", - "@react-types/shared": "^3.19.0", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.13.tgz", + "integrity": "sha512-GUwY7sQtPMtO6LFHyoIGFMEv8tEBrNCrSNwEKilFLxvNUCo/1sY3N+7L2TcoeyDkcRWBJ9Uz9iR0iJ6EsCBWng==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/overlays": "^3.22.0", + "@react-aria/utils": "^3.24.0", + "@react-types/dialog": "^3.5.9", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-aria/focus": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.14.0.tgz", - "integrity": "sha512-Xw7PxLT0Cqcz22OVtTZ8+HvurDogn9/xntzoIbVjpRFWzhlYe5WHnZL+2+gIiKf7EZ18Ma9/QsCnrVnvrky/Kw==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.17.0.tgz", + "integrity": "sha512-aRzBw1WTUkcIV3xFrqPA6aB8ZVt3XyGpTaSHAypU0Pgoy2wRq9YeJYpbunsKj9CJmskuffvTqXwAjTcaQish1Q==", "dependencies": { - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-types/shared": "^3.19.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/utils": "^3.24.0", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0", - "clsx": "^1.1.1" + "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-aria/grid": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.8.1.tgz", - "integrity": "sha512-J/k7i2ZnMgTv3csMIQrIanbb0mWzlokT86QfKDgQpKxIvrPGbdrVJTx99tzJxEzYeXN9w11Jjwjal65rZCs4rQ==", + "node_modules/@react-aria/form": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@react-aria/form/-/form-3.0.4.tgz", + "integrity": "sha512-wWfW9Hv+OWIUbJ0QYzJ4EO5Yt7xZD1i+XNZG9pKGBiREi7dYBo7Y7lbqlWc3pJASSE+6aP9HzhK18dMPtGluVA==", "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/i18n": "^3.8.1", - "@react-aria/interactions": "^3.17.0", - "@react-aria/live-announcer": "^3.3.1", - "@react-aria/selection": "^3.16.1", - "@react-aria/utils": "^3.19.0", - "@react-stately/collections": "^3.10.0", - "@react-stately/grid": "^3.8.0", - "@react-stately/selection": "^3.13.3", - "@react-stately/virtualizer": "^3.6.1", - "@react-types/checkbox": "^3.5.0", - "@react-types/grid": "^3.2.0", - "@react-types/shared": "^3.19.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/utils": "^3.24.0", + "@react-stately/form": "^3.0.2", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/grid": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.9.0.tgz", + "integrity": "sha512-jNg7haMptmeTKR7/ZomIjWZMLB6jWalBkl5in2JdU9Hc4pY5EKqD/7PSprr9SjOzCr5O+4MSiRDvw+Tu7xHevQ==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/i18n": "^3.11.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/live-announcer": "^3.3.3", + "@react-aria/selection": "^3.18.0", + "@react-aria/utils": "^3.24.0", + "@react-stately/collections": "^3.10.6", + "@react-stately/grid": "^3.8.6", + "@react-stately/selection": "^3.15.0", + "@react-stately/virtualizer": "^3.7.0", + "@react-types/checkbox": "^3.8.0", + "@react-types/grid": "^3.2.5", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -1967,17 +2478,17 @@ } }, "node_modules/@react-aria/i18n": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.8.1.tgz", - "integrity": "sha512-ftH3saJlhWaHoHEDb/YjYqP8I4/9t4Ksf0D0kvPDRfRcL98DKUSHZD77+EmbjsmzJInzm76qDeEV0FYl4oj7gg==", - "dependencies": { - "@internationalized/date": "^3.4.0", - "@internationalized/message": "^3.1.1", - "@internationalized/number": "^3.2.1", - "@internationalized/string": "^3.1.1", - "@react-aria/ssr": "^3.7.1", - "@react-aria/utils": "^3.19.0", - "@react-types/shared": "^3.19.0", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.11.0.tgz", + "integrity": "sha512-dnopopsYKy2cd2dB2LdnmdJ58evKKcNCtiscWl624XFSbq2laDrYIQ4umrMhBxaKD7nDQkqydVBe6HoQKPzvJw==", + "dependencies": { + "@internationalized/date": "^3.5.3", + "@internationalized/message": "^3.1.3", + "@internationalized/number": "^3.5.2", + "@internationalized/string": "^3.2.2", + "@react-aria/ssr": "^3.9.3", + "@react-aria/utils": "^3.24.0", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -1985,13 +2496,13 @@ } }, "node_modules/@react-aria/interactions": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.17.0.tgz", - "integrity": "sha512-v4BI5Nd8gi8s297fHpgjDDXOyufX+FPHJ31rkMwY6X1nR5gtI0+2jNOL4lh7s+cWzszpA0wpwIrKUPGhhLyUjQ==", + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.21.2.tgz", + "integrity": "sha512-Ju706DtoEmI/2vsfu9DCEIjDqsRBVLm/wmt2fr0xKbBca7PtmK8daajxFWz+eTq+EJakvYfLr7gWgLau9HyWXg==", "dependencies": { - "@react-aria/ssr": "^3.7.1", - "@react-aria/utils": "^3.19.0", - "@react-types/shared": "^3.19.0", + "@react-aria/ssr": "^3.9.3", + "@react-aria/utils": "^3.24.0", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -1999,13 +2510,12 @@ } }, "node_modules/@react-aria/label": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.6.1.tgz", - "integrity": "sha512-hR7Qx6q0BjOJi/YG5pI13QTQA/2oaXMYdzDCx4Faz8qaY9CCsLjFpo5pUUwRhNieGmf/nHJq6jiYbJqfaONuTQ==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.7.7.tgz", + "integrity": "sha512-0MDIu4SbagwsYzkprcCzi1Z0V/t2K/5Dd30eSTL2zanXMa+/85MVGSQjXI0vPrXMOXSNqp0R/aMxcqcgJ59yRA==", "dependencies": { - "@react-aria/utils": "^3.19.0", - "@react-types/label": "^3.7.5", - "@react-types/shared": "^3.19.0", + "@react-aria/utils": "^3.24.0", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2013,46 +2523,66 @@ } }, "node_modules/@react-aria/link": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.5.3.tgz", - "integrity": "sha512-WGz/s/czlb/+wJUnBfnfaRuvOSiNTaQDTk9QsEEwrTkkYbWo7fMlH5Tc7c0Uxem4UuUblYXKth5SskiKQNWc0w==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-types/link": "^3.4.4", - "@react-types/shared": "^3.19.0", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.7.0.tgz", + "integrity": "sha512-gkF7KpDR+ApcMY5HS3xVKHrxRcwSP9TRPoySWEMBE4GPWvEK1Bk/On9EM1vRzeEibCZ5L6gKuLEEKLVSGbBMWg==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/utils": "^3.24.0", + "@react-types/link": "^3.5.4", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@react-aria/listbox": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-aria/listbox/-/listbox-3.12.0.tgz", + "integrity": "sha512-Cy+UcfXU4MrOBMBnaB+kqG8bajeS3T1ZN8L7PXSTpmFS9jShFMhYkNz5gXpI+0SS4dgbHtkq/YDFJvu+bxFvdg==", + "dependencies": { + "@react-aria/interactions": "^3.21.2", + "@react-aria/label": "^3.7.7", + "@react-aria/selection": "^3.18.0", + "@react-aria/utils": "^3.24.0", + "@react-stately/collections": "^3.10.6", + "@react-stately/list": "^3.10.4", + "@react-types/listbox": "^3.4.8", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, "node_modules/@react-aria/live-announcer": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.3.1.tgz", - "integrity": "sha512-hsc77U7S16trM86d+peqJCOCQ7/smO1cybgdpOuzXyiwcHQw8RQ4GrXrS37P4Ux/44E9nMZkOwATQRT2aK8+Ew==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.3.3.tgz", + "integrity": "sha512-sMaBzzIgDPBDCeZ/UFbuXR/UnXikcE7t4OJ4cESzmUq6r6LvxzmZnG9ocwpH75n7udmUbINycKD082fneryHlg==", "dependencies": { "@swc/helpers": "^0.5.0" } }, "node_modules/@react-aria/menu": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.10.1.tgz", - "integrity": "sha512-FOb16XVejZgl4sFpclLvGd2RCvUBwl2bzFdAnss8Nd6Mx+h4m0bPeDT102k9v1Vjo7OGeqzvMyNU/KM4FwUGGA==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/i18n": "^3.8.1", - "@react-aria/interactions": "^3.17.0", - "@react-aria/overlays": "^3.16.0", - "@react-aria/selection": "^3.16.1", - "@react-aria/utils": "^3.19.0", - "@react-stately/collections": "^3.10.0", - "@react-stately/menu": "^3.5.4", - "@react-stately/tree": "^3.7.1", - "@react-types/button": "^3.7.4", - "@react-types/menu": "^3.9.3", - "@react-types/shared": "^3.19.0", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.14.0.tgz", + "integrity": "sha512-veZIpwKPKDIX1xpUzvGnxSVTmMfpRjPQUi1v+hMgqgdjBKedKI2LkprLABo9grggjqV9c2xT4XUXDk6xH3r8eA==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/i18n": "^3.11.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/overlays": "^3.22.0", + "@react-aria/selection": "^3.18.0", + "@react-aria/utils": "^3.24.0", + "@react-stately/collections": "^3.10.6", + "@react-stately/menu": "^3.7.0", + "@react-stately/tree": "^3.8.0", + "@react-types/button": "^3.9.3", + "@react-types/menu": "^3.9.8", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2061,20 +2591,20 @@ } }, "node_modules/@react-aria/overlays": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.16.0.tgz", - "integrity": "sha512-jclyCqs1U4XqDA1DAdZaiijKtHLVZ78FV0+IzL4QQfrvzCPC+ba+MC8pe/tw8dMQzXBSnTx/IEqOHu07IwrESQ==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/i18n": "^3.8.1", - "@react-aria/interactions": "^3.17.0", - "@react-aria/ssr": "^3.7.1", - "@react-aria/utils": "^3.19.0", - "@react-aria/visually-hidden": "^3.8.3", - "@react-stately/overlays": "^3.6.1", - "@react-types/button": "^3.7.4", - "@react-types/overlays": "^3.8.1", - "@react-types/shared": "^3.19.0", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.22.0.tgz", + "integrity": "sha512-M3Iayc2Hf9vJ4JJ8K/zh+Ct6aymDLmBbo686ChV3AtGOc254RyyzqnVSNuMs3j5QVBsDUKihHZQfl4E9RCwd+w==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/i18n": "^3.11.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/ssr": "^3.9.3", + "@react-aria/utils": "^3.24.0", + "@react-aria/visually-hidden": "^3.8.11", + "@react-stately/overlays": "^3.6.6", + "@react-types/button": "^3.9.3", + "@react-types/overlays": "^3.8.6", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2083,15 +2613,15 @@ } }, "node_modules/@react-aria/progress": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.4.tgz", - "integrity": "sha512-k4EBtYcmqw3j/JYJtn+xKPM8/P1uPcFGSBqvwmVdwDknuT/hR1os3wIKm712N/Ubde8hTeeLcaa38HYezSF8BA==", - "dependencies": { - "@react-aria/i18n": "^3.8.1", - "@react-aria/label": "^3.6.1", - "@react-aria/utils": "^3.19.0", - "@react-types/progress": "^3.4.2", - "@react-types/shared": "^3.19.0", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.12.tgz", + "integrity": "sha512-Wlz7VNFEzcLSawhZwWTKgJPM/IUKFiKJJG7KGcsT2biIlu6Yp60xj08hDZkCrLq3XsLLCRmweHlVfLFjG3AK9w==", + "dependencies": { + "@react-aria/i18n": "^3.11.0", + "@react-aria/label": "^3.7.7", + "@react-aria/utils": "^3.24.0", + "@react-types/progress": "^3.5.3", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2099,18 +2629,19 @@ } }, "node_modules/@react-aria/radio": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.7.0.tgz", - "integrity": "sha512-ygSr3ow9avO5BNNwm4aL70EwvLHrBbhSVfG1lmP2k5u/2dxn+Pnm3BGMaEriOFiAyAV4nLGUZAjER6GWXfu5cA==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/i18n": "^3.8.1", - "@react-aria/interactions": "^3.17.0", - "@react-aria/label": "^3.6.1", - "@react-aria/utils": "^3.19.0", - "@react-stately/radio": "^3.8.3", - "@react-types/radio": "^3.5.0", - "@react-types/shared": "^3.19.0", + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.10.3.tgz", + "integrity": "sha512-9noof5jyHE8iiFEUE7xCAHvCjG7EkZ/bZHh2+ZtrLlTFZmjpEbRbpZMw6QMKC8uzREPsmERBXjbd/6NyXH6mEQ==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/form": "^3.0.4", + "@react-aria/i18n": "^3.11.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/label": "^3.7.7", + "@react-aria/utils": "^3.24.0", + "@react-stately/radio": "^3.10.3", + "@react-types/radio": "^3.8.0", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2118,27 +2649,63 @@ } }, "node_modules/@react-aria/selection": { - "version": "3.16.1", - "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.16.1.tgz", - "integrity": "sha512-mOoAeNjq23H5p6IaeoyLHavYHRXOuNUlv8xO4OzYxIEnxmAvk4PCgidGLFYrr4sloftUMgTTL3LpCj21ylBS9A==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/i18n": "^3.8.1", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/collections": "^3.10.0", - "@react-stately/selection": "^3.13.3", - "@react-types/shared": "^3.19.0", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.18.0.tgz", + "integrity": "sha512-6ZvRuS9OHe56UVTb/qnsZ1TOxpZH9gRlX6eGG3Pt4LZK12wcvs13Uz2OvB2aYQHu0KPAua9ACnPh94xvXzQIlQ==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/i18n": "^3.11.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/utils": "^3.24.0", + "@react-stately/selection": "^3.15.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/slider": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.7.7.tgz", + "integrity": "sha512-7tOJyR4ZZoSMKcVomC6DZxyYuXQqQopi9mPW2J1fViD1R5iO8YVmoX/ALXnokzi8GPuMA0c38i2Cmnecm30ZXA==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/i18n": "^3.11.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/label": "^3.7.7", + "@react-aria/utils": "^3.24.0", + "@react-stately/slider": "^3.5.3", + "@react-types/shared": "^3.23.0", + "@react-types/slider": "^3.7.2", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@react-aria/spinbutton": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.6.4.tgz", + "integrity": "sha512-KMnwm3bEM83g8MILGt6irbvAG7DNphkq6O0ePt7L1m6QZhWK3hbI2RNlxYMF1OKIDTAOhnEjR6IdMCWt9TuXvQ==", + "dependencies": { + "@react-aria/i18n": "^3.11.0", + "@react-aria/live-announcer": "^3.3.3", + "@react-aria/utils": "^3.24.0", + "@react-types/button": "^3.9.3", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, "node_modules/@react-aria/ssr": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.7.1.tgz", - "integrity": "sha512-ovVPSD1WlRpZHt7GI9DqJrWG3OIYS+NXQ9y5HIewMJpSe+jPQmMQfyRmgX4EnvmxSlp0u04Wg/7oItcoSIb/RA==", + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.3.tgz", + "integrity": "sha512-5bUZ93dmvHFcmfUcEN7qzYe8yQQ8JY+nHN6m9/iSDCQ/QmCiE0kWXYwhurjw5ch6I8WokQzx66xKIMHBAa4NNA==", "dependencies": { "@swc/helpers": "^0.5.0" }, @@ -2150,13 +2717,13 @@ } }, "node_modules/@react-aria/switch": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.5.3.tgz", - "integrity": "sha512-3sV78Oa12/aU+M9P7BqUDdp/zm2zZA2QvtLLdxykrH04AJp0hLNBnmaTDXJVaGPPiU0umOB0LWDquA3apkBiBA==", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.6.3.tgz", + "integrity": "sha512-UBWbTEnnlTDT/dFOEpGKfX5ngPTIOVDLX1ltUhDHHk6SrgSnvYxTPTZAo+ujHIUSBFHOuxmvVYG7y54rk168mg==", "dependencies": { - "@react-aria/toggle": "^3.7.0", - "@react-stately/toggle": "^3.6.1", - "@react-types/switch": "^3.4.0", + "@react-aria/toggle": "^3.10.3", + "@react-stately/toggle": "^3.7.3", + "@react-types/switch": "^3.5.2", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2164,26 +2731,25 @@ } }, "node_modules/@react-aria/table": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.11.0.tgz", - "integrity": "sha512-kPIQWh1dIHFAzl+rzfUGgbpAZGerMwwW0zNvRwcLpBOl/nrOwV5Zg/wuCC5cSdkwgo3SghYbcUaM19teve0UcQ==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/grid": "^3.8.1", - "@react-aria/i18n": "^3.8.1", - "@react-aria/interactions": "^3.17.0", - "@react-aria/live-announcer": "^3.3.1", - "@react-aria/selection": "^3.16.1", - "@react-aria/utils": "^3.19.0", - "@react-aria/visually-hidden": "^3.8.3", - "@react-stately/collections": "^3.10.0", - "@react-stately/flags": "^3.0.0", - "@react-stately/table": "^3.11.0", - "@react-stately/virtualizer": "^3.6.1", - "@react-types/checkbox": "^3.5.0", - "@react-types/grid": "^3.2.0", - "@react-types/shared": "^3.19.0", - "@react-types/table": "^3.8.0", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.14.0.tgz", + "integrity": "sha512-IwBmzeIxeZjWlOlmMXVj/L64FbYm3qUh7v3VRgU98BVOdvgUyEKBDIwi6SuOV4FwbXKrCPZbXPU/k+KQU4tUoQ==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/grid": "^3.9.0", + "@react-aria/i18n": "^3.11.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/live-announcer": "^3.3.3", + "@react-aria/utils": "^3.24.0", + "@react-aria/visually-hidden": "^3.8.11", + "@react-stately/collections": "^3.10.6", + "@react-stately/flags": "^3.0.2", + "@react-stately/table": "^3.11.7", + "@react-stately/virtualizer": "^3.7.0", + "@react-types/checkbox": "^3.8.0", + "@react-types/grid": "^3.2.5", + "@react-types/shared": "^3.23.0", + "@react-types/table": "^3.9.4", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2192,35 +2758,37 @@ } }, "node_modules/@react-aria/tabs": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.6.2.tgz", - "integrity": "sha512-FjI0h1Z4TsLOvIODhdDrVLz0O8RAqxDi58DO88CwkdUrWwZspNEpSpHhDarzUT7MlX3X72lsAUwvQLqY1OmaBQ==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/i18n": "^3.8.1", - "@react-aria/interactions": "^3.17.0", - "@react-aria/selection": "^3.16.1", - "@react-aria/utils": "^3.19.0", - "@react-stately/list": "^3.9.1", - "@react-stately/tabs": "^3.5.1", - "@react-types/shared": "^3.19.0", - "@react-types/tabs": "^3.3.1", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.9.0.tgz", + "integrity": "sha512-E4IHOO9ejEXNeSnpeThu79pDpNySHHYz3txr9ngtH6tp097k/I1auSqbGJPy/kwLj6MCPEt86dNJDXE2X0AcFw==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/i18n": "^3.11.0", + "@react-aria/selection": "^3.18.0", + "@react-aria/utils": "^3.24.0", + "@react-stately/tabs": "^3.6.5", + "@react-types/shared": "^3.23.0", + "@react-types/tabs": "^3.3.6", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-aria/textfield": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.11.0.tgz", - "integrity": "sha512-07pHRuWeLmsmciWL8y9azUwcBYi1IBmOT9KxBgLdLK5NLejd7q2uqd0WEEgZkOc48i2KEtMDgBslc4hA+cmHow==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/label": "^3.6.1", - "@react-aria/utils": "^3.19.0", - "@react-types/shared": "^3.19.0", - "@react-types/textfield": "^3.7.3", + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.14.4.tgz", + "integrity": "sha512-fdZChDyTRA4BPqbyDeD9gSw6rVeIAl7eG38osRwr0mzcKTiS/AyV3jiRwnHsBO9brU8RdViJFri4emVDuxSjag==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/form": "^3.0.4", + "@react-aria/label": "^3.7.7", + "@react-aria/utils": "^3.24.0", + "@react-stately/form": "^3.0.2", + "@react-stately/utils": "^3.10.0", + "@react-types/shared": "^3.23.0", + "@react-types/textfield": "^3.9.2", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2228,17 +2796,15 @@ } }, "node_modules/@react-aria/toggle": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.7.0.tgz", - "integrity": "sha512-8Rpqolm8dxesyHi03RSmX2MjfHO/YwdhyEpAMMO0nsajjdtZneGzIOXzyjdWCPWwwzahcpwRHOA4qfMiRz+axA==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/toggle": "^3.6.1", - "@react-types/checkbox": "^3.5.0", - "@react-types/shared": "^3.19.0", - "@react-types/switch": "^3.4.0", + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.3.tgz", + "integrity": "sha512-QtufHlWczMcTGmRxF7RCEgfMKpUPivyXJWZsQ1HSlknjRJPzf4uc3mSR62hq2sZ0VN9zXEpUsoixbEDB87TnGg==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/utils": "^3.24.0", + "@react-stately/toggle": "^3.7.3", + "@react-types/checkbox": "^3.8.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2246,16 +2812,16 @@ } }, "node_modules/@react-aria/tooltip": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.6.1.tgz", - "integrity": "sha512-CVSmndGXhC5EkkGrKcC8EVdAKCbSLTyJibpojC/8uOCbGIQglq3xCAr68PElNNO8+sFDJ4fp9ZzEeDi0Qyxf0w==", - "dependencies": { - "@react-aria/focus": "^3.14.0", - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-stately/tooltip": "^3.4.3", - "@react-types/shared": "^3.19.0", - "@react-types/tooltip": "^3.4.3", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.7.3.tgz", + "integrity": "sha512-uF2J/GRKTHSeEYMwvXTu7oK710nov/NRbY7db2Hh7yXluGmjJORXb5wxsy+lqHaWqPKBbkhmxBJYeJJpAqlZ5g==", + "dependencies": { + "@react-aria/focus": "^3.17.0", + "@react-aria/interactions": "^3.21.2", + "@react-aria/utils": "^3.24.0", + "@react-stately/tooltip": "^3.4.8", + "@react-types/shared": "^3.23.0", + "@react-types/tooltip": "^3.4.8", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2263,45 +2829,44 @@ } }, "node_modules/@react-aria/utils": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.19.0.tgz", - "integrity": "sha512-5GXqTCrUQtr78aiLVHZoeeGPuAxO4lCM+udWbKpSCh5xLfCZ7zFlZV9Q9FS0ea+IQypUcY8ngXCLsf22nSu/yg==", + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.24.0.tgz", + "integrity": "sha512-JAxkPhK5fCvFVNY2YG3TW3m1nTzwRcbz7iyTSkUzLFat4N4LZ7Kzh7NMHsgeE/oMOxd8zLY+XsUxMu/E/2GujA==", "dependencies": { - "@react-aria/ssr": "^3.7.1", - "@react-stately/utils": "^3.7.0", - "@react-types/shared": "^3.19.0", + "@react-aria/ssr": "^3.9.3", + "@react-stately/utils": "^3.10.0", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0", - "clsx": "^1.1.1" + "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-aria/visually-hidden": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.3.tgz", - "integrity": "sha512-Ln3rqUnPF/UiiPjj8Xjc5FIagwNvG16qtAR2Diwnsju+X9o2xeDEZhN/5fg98PxH2JBS3IvtsmMZRzPT9mhpmg==", + "version": "3.8.11", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.11.tgz", + "integrity": "sha512-1JFruyAatoKnC18qrix8Q1gyUNlizWZvYdPADgB5btakMy0PEGTWPmFRK5gFsO+N0CZLCFTCip0dkUv6rrp31w==", "dependencies": { - "@react-aria/interactions": "^3.17.0", - "@react-aria/utils": "^3.19.0", - "@react-types/shared": "^3.19.0", - "@swc/helpers": "^0.5.0", - "clsx": "^1.1.1" + "@react-aria/interactions": "^3.21.2", + "@react-aria/utils": "^3.24.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-google-maps/api": { - "version": "2.19.2", - "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.19.2.tgz", - "integrity": "sha512-Vt57XWzCKfsUjKOmFUl2erVVfOePkPK5OigF/f+q7UuV/Nm9KDDy1PMFBx+wNahEqOd6a32BxfsykEhBnbU9wQ==", + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.19.3.tgz", + "integrity": "sha512-jiLqvuOt5lOowkLeq7d077AByTyJp+s6hZVlLhlq7SBacBD37aUNpXBz2OsazfeR6Aw4a+9RRhAEjEFvrR1f5A==", "dependencies": { "@googlemaps/js-api-loader": "1.16.2", - "@googlemaps/markerclusterer": "2.3.2", + "@googlemaps/markerclusterer": "2.5.3", "@react-google-maps/infobox": "2.19.2", "@react-google-maps/marker-clusterer": "2.19.2", - "@types/google.maps": "3.53.5", + "@types/google.maps": "3.55.2", "invariant": "2.2.4" }, "peerDependencies": { @@ -2319,15 +2884,31 @@ "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.19.2.tgz", "integrity": "sha512-x9ibmsP0ZVqzyCo1Pitbw+4b6iEXRw/r1TCy3vOUR3eKrzWLnHYZMR325BkZW2r8fnuWE/V3Fp4QZOP9qYORCw==" }, + "node_modules/@react-stately/calendar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.4.1.tgz", + "integrity": "sha512-XKCdrXNA7/ukZ842EeDZfLqYUQDv/x5RoAVkzTbp++3U/MLM1XZXsqj+5xVlQfJiWpQzM9L6ySjxzzgepJDeuw==", + "dependencies": { + "@internationalized/date": "^3.5.0", + "@react-stately/utils": "^3.8.0", + "@react-types/calendar": "^3.4.1", + "@react-types/datepicker": "^3.6.1", + "@react-types/shared": "^3.21.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, "node_modules/@react-stately/checkbox": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.4.4.tgz", - "integrity": "sha512-TYNod4+4TmS73F+sbKXAMoBH810ZEBdpMfXlNttUCXfVkDXc38W7ucvpQxXPwF+d+ZhGk4DJZsUYqfVPyXXSGg==", - "dependencies": { - "@react-stately/toggle": "^3.6.1", - "@react-stately/utils": "^3.7.0", - "@react-types/checkbox": "^3.5.0", - "@react-types/shared": "^3.19.0", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.6.4.tgz", + "integrity": "sha512-gecaRtWeQNoJuSl3AtfV6z6LjaUV578Kzbag8d3pTPbGXl8komTtTj/26nIEPsmf/L8jZ3kCscDGxGTKr+7sqg==", + "dependencies": { + "@react-stately/form": "^3.0.2", + "@react-stately/utils": "^3.10.0", + "@react-types/checkbox": "^3.8.0", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2335,11 +2916,48 @@ } }, "node_modules/@react-stately/collections": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.10.0.tgz", - "integrity": "sha512-PyJEFmt9X0kDMF7D4StGnTdXX1hgyUcTXvvXU2fEw6OyXLtmfWFHmFARRtYbuelGKk6clmJojYmIEds0k8jdww==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.10.6.tgz", + "integrity": "sha512-hb/yzxQnZaSRu43iR6ftkCJIqD4Qu5WUjl4ASBn2EGb9TmipA7bFnYVqSH4xFPCCTZ68Qxh95dOcxYBHlHeWZQ==", "dependencies": { - "@react-types/shared": "^3.19.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/combobox": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.8.3.tgz", + "integrity": "sha512-lmwt2M39jHQUA9CWKhTc9MVoUBKuJM1Y+9GYPElON8P/guQL6G3bM1u8I4Hxf0zzGzAIW3ymV57bF9mcaA/nzA==", + "dependencies": { + "@react-stately/collections": "^3.10.6", + "@react-stately/form": "^3.0.2", + "@react-stately/list": "^3.10.4", + "@react-stately/overlays": "^3.6.6", + "@react-stately/select": "^3.6.3", + "@react-stately/utils": "^3.10.0", + "@react-types/combobox": "^3.11.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/datepicker": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.9.3.tgz", + "integrity": "sha512-NjZ8uqxmKf7mGLNWSZsvm22xX46k+yo0QkPspONuorHFTf8qqCnp4i+bBpEpaVCwX5KVSRdjxJOk7XhvJF8q4w==", + "dependencies": { + "@internationalized/date": "^3.5.3", + "@internationalized/string": "^3.2.2", + "@react-stately/form": "^3.0.2", + "@react-stately/overlays": "^3.6.6", + "@react-stately/utils": "^3.10.0", + "@react-types/datepicker": "^3.7.3", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2347,9 +2965,9 @@ } }, "node_modules/@react-stately/flags": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.0.0.tgz", - "integrity": "sha512-e3i2ItHbIa0eEwmSXAnPdD7K8syW76JjGe8ENxwFJPW/H1Pu9RJfjkCb/Mq0WSPN/TpxBb54+I9TgrGhbCoZ9w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.0.2.tgz", + "integrity": "sha512-/KyVJMND2WdkgoHpt+m+ash7h5q9pq91DLgyizQWcbf2xphicH9D1HKAB8co3Cfvq6T/QqjQEP8aBkheiPyfEg==", "dependencies": { "@swc/helpers": "^0.4.14" } @@ -2363,389 +2981,509 @@ "tslib": "^2.4.0" } }, - "node_modules/@react-stately/grid": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.8.0.tgz", - "integrity": "sha512-+3Q6D3W5FTc9/t1Gz35sH0NRiJ2u95aDls9ogBNulC/kQvYaF31NT34QdvpstcfrcCFtF+D49+TkesklZRHJlw==", + "node_modules/@react-stately/form": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.0.2.tgz", + "integrity": "sha512-MA4P9lHv770I3DJpJTQlkh5POVuklmeQuixwlbyKzlWT+KqFSOXvqaliszqU7gyDdVGAFksMa6E3mXbGbk1wuA==", + "dependencies": { + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/grid": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.8.6.tgz", + "integrity": "sha512-XkxDfaIAWzbsb5pnL2IE4FqQbqegVzPnU+R2ZvDrJT7514I2usSMoJ2ZUUoy8DIYQomJHB5QKZeyQkGIelHMcg==", + "dependencies": { + "@react-stately/collections": "^3.10.6", + "@react-stately/selection": "^3.15.0", + "@react-types/grid": "^3.2.5", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/list": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.10.4.tgz", + "integrity": "sha512-sj501OKcQr+1Zdo0m6NuvpZDHLE0tUdReSKcWqt35odzC6ic/qr7C7ozZ/5ay+nuHTryUUTC/mDQ0zlBmQX0dA==", + "dependencies": { + "@react-stately/collections": "^3.10.6", + "@react-stately/selection": "^3.15.0", + "@react-stately/utils": "^3.10.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/menu": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.7.0.tgz", + "integrity": "sha512-8UJhvKEF+zaHXrwv0YhFr73OSEprzIs6xRNoV6F/omd4twy1ngPZrL1X8HNzaXsf5BrHuib2tbh81e/Z95D3nA==", + "dependencies": { + "@react-stately/overlays": "^3.6.6", + "@react-types/menu": "^3.9.8", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/overlays": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.6.tgz", + "integrity": "sha512-NvzQXh4zYGZuUmZH5d3NmEDNr8r1hfub2s5w7WOeIG35xqIzoKGdFZ7LLWrie+4nxPmM+ckdfqOQ9pBZFNJypQ==", + "dependencies": { + "@react-stately/utils": "^3.10.0", + "@react-types/overlays": "^3.8.6", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/radio": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.10.3.tgz", + "integrity": "sha512-EWLLRgLQ9orI7G9uPuJv1bdZPu3OoRWy1TGSn+6G8b8rleNx3haI4eZUR+JGB0YNgemotMz/gbNTNG/wEIsRgw==", + "dependencies": { + "@react-stately/form": "^3.0.2", + "@react-stately/utils": "^3.10.0", + "@react-types/radio": "^3.8.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/select": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@react-stately/select/-/select-3.6.3.tgz", + "integrity": "sha512-d/ha6j0oiEaw/F5hgPgCZg1e8LObNmvsocEebxXPToVdwHd9H55r2Fogi5nLoiX8geHKiYm0KPfSxs/oXbW/5Q==", + "dependencies": { + "@react-stately/form": "^3.0.2", + "@react-stately/list": "^3.10.4", + "@react-stately/overlays": "^3.6.6", + "@react-types/select": "^3.9.3", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/selection": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.15.0.tgz", + "integrity": "sha512-OtypXNtvRWLmpkaktluzCYEXKXAON16WIJv2mZ4cae3H0UVfWaFL9sD+ST9nj7UqYNTDXECug5ziIY+YKd7zvA==", + "dependencies": { + "@react-stately/collections": "^3.10.6", + "@react-stately/utils": "^3.10.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/slider": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@react-stately/slider/-/slider-3.5.3.tgz", + "integrity": "sha512-jA0XR7GjtwoucLw8kx/KB50pSGNUbR7xNfM9t5H8D7k3wd+j4yqfarWyNFyPX/X5MJez+/bd+BIDJUl3XGOWkA==", "dependencies": { - "@react-stately/collections": "^3.10.0", - "@react-stately/selection": "^3.13.3", - "@react-types/grid": "^3.2.0", - "@react-types/shared": "^3.19.0", + "@react-stately/utils": "^3.10.0", + "@react-types/shared": "^3.23.0", + "@react-types/slider": "^3.7.2", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/list": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.9.1.tgz", - "integrity": "sha512-GiKrxGakzMTZKe3mp410l4xKiHbZplJCGrtqlxq/+YRD0uCQwWGYpRG+z9A7tTCusruRD3m91/OjWsbfbGdiEw==", - "dependencies": { - "@react-stately/collections": "^3.10.0", - "@react-stately/selection": "^3.13.3", - "@react-stately/utils": "^3.7.0", - "@react-types/shared": "^3.19.0", + "node_modules/@react-stately/table": { + "version": "3.11.7", + "resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.11.7.tgz", + "integrity": "sha512-VvazamtoXLENeWJAYF1fJzfIAXO2qbiXCfosRLgkEMtoU2kGqV8DHYQhIXuqwMRn8nO8GVw9hgAiQQcKghgCXA==", + "dependencies": { + "@react-stately/collections": "^3.10.6", + "@react-stately/flags": "^3.0.2", + "@react-stately/grid": "^3.8.6", + "@react-stately/selection": "^3.15.0", + "@react-stately/utils": "^3.10.0", + "@react-types/grid": "^3.2.5", + "@react-types/shared": "^3.23.0", + "@react-types/table": "^3.9.4", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/menu": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.5.4.tgz", - "integrity": "sha512-+Q71fMDhMM1iARPFtwqpXY/8qkb0dN4PBJbcjwjGCumGs+ja2YbZxLBHCP0DYBElS9l6m3ssF47RKNMtF/Oi5w==", + "node_modules/@react-stately/tabs": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.6.5.tgz", + "integrity": "sha512-z1saZgGRqb0AsoRi19iE4JOJaIRV73GjRnzUX9QSl3gpK75XsH31vbmtUYiXOXAd6Dt+1KFLgbyeCzMUlZEnMw==", "dependencies": { - "@react-stately/overlays": "^3.6.1", - "@react-stately/utils": "^3.7.0", - "@react-types/menu": "^3.9.3", - "@react-types/shared": "^3.19.0", + "@react-stately/list": "^3.10.4", + "@react-types/shared": "^3.23.0", + "@react-types/tabs": "^3.3.6", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/overlays": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.1.tgz", - "integrity": "sha512-c/Mda4ZZmFO4e3XZFd7kqt5wuh6Q/7wYJ+0oG59MfDoQstFwGcJTUnx7S8EUMujbocIOCeOmVPA1eE3DNPC2/A==", + "node_modules/@react-stately/toggle": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.7.3.tgz", + "integrity": "sha512-4jW6wxTu7Gkq6/2mZWqtJoQ6ff27Cl6lnVMEXXM+M8HwK/3zHoMZhVz8EApwgOsRByxDQ76PNSGm3xKZAcqZNw==", "dependencies": { - "@react-stately/utils": "^3.7.0", - "@react-types/overlays": "^3.8.1", + "@react-stately/utils": "^3.10.0", + "@react-types/checkbox": "^3.8.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/radio": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.8.3.tgz", - "integrity": "sha512-3ovJ6tDWzl/Qap8065GZS9mQM7LbQwLc7EhhmQ3dn5+pH4pUCHo8Gb0TIcYFsvFMyHrNMg/r8+N3ICq/WDj5NQ==", + "node_modules/@react-stately/tooltip": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.4.8.tgz", + "integrity": "sha512-0D3cCeQhX5DjDpeuzFJwfX8SxIOxdL2iWPPjpC3hIxkUKuItavSq2A7G2tO39vpiip3RBOaaQMUpnSmjRK5DAQ==", "dependencies": { - "@react-stately/utils": "^3.7.0", - "@react-types/radio": "^3.5.0", - "@react-types/shared": "^3.19.0", + "@react-stately/overlays": "^3.6.6", + "@react-types/tooltip": "^3.4.8", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/selection": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.13.3.tgz", - "integrity": "sha512-+CmpZpyIXfbxEwd9eBvo5Jatc2MNX7HinBcW3X8GfvqNzkbgOXETsmXaW6jlKJekvLLE13Is78Ob8NNzZVxQYg==", + "node_modules/@react-stately/tree": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.8.0.tgz", + "integrity": "sha512-7bfbCLjG8BTiWuo9GBE1A375PPI4S9r/rMtKQGLQvYAObgJb7C8P3svA9WKfryvl7M5iqaYrOVA0uzNSmeCNQQ==", "dependencies": { - "@react-stately/collections": "^3.10.0", - "@react-stately/utils": "^3.7.0", - "@react-types/shared": "^3.19.0", + "@react-stately/collections": "^3.10.6", + "@react-stately/selection": "^3.15.0", + "@react-stately/utils": "^3.10.0", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/table": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.11.0.tgz", - "integrity": "sha512-mHv8KgNHm6scO0gntQc1ZVbQaAqLiNzYi4hxksz2lY+HN2CJbJkYGl/aRt4jmnfpi1xWpwYP5najXdncMAKpGA==", - "dependencies": { - "@react-stately/collections": "^3.10.0", - "@react-stately/flags": "^3.0.0", - "@react-stately/grid": "^3.8.0", - "@react-stately/selection": "^3.13.3", - "@react-stately/utils": "^3.7.0", - "@react-types/grid": "^3.2.0", - "@react-types/shared": "^3.19.0", - "@react-types/table": "^3.8.0", + "node_modules/@react-stately/utils": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.0.tgz", + "integrity": "sha512-nji2i9fTYg65ZWx/3r11zR1F2tGya+mBubRCbMTwHyRnsSLFZaeq/W6lmrOyIy1uMJKBNKLJpqfmpT4x7rw6pg==", + "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/tabs": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.5.1.tgz", - "integrity": "sha512-p1vZOuIS98GMF9jfEHQA6Pir1wYY6j+Gni6DcluNnWj90rLEubuwARNw7uscoOaXKlK/DiZIhkLKSDsA5tbadQ==", + "node_modules/@react-stately/virtualizer": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@react-stately/virtualizer/-/virtualizer-3.7.0.tgz", + "integrity": "sha512-Wkh502y6mzUvjJJr30p5FLRwBaphnfmnoSnGwidamwo3HuyrDICBSlwFGPl0AmUHo1afSaLXl6j8smU48VcClA==", "dependencies": { - "@react-stately/list": "^3.9.1", - "@react-stately/utils": "^3.7.0", - "@react-types/shared": "^3.19.0", - "@react-types/tabs": "^3.3.1", + "@react-aria/utils": "^3.24.0", + "@react-types/shared": "^3.23.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/toggle": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.6.1.tgz", - "integrity": "sha512-UUWtuI6gZlX6wpF9/bxBikjyAW1yQojRPCJ4MPkjMMBQL0iveAm3WEQkXRLNycEiOCeoaVFBwAd1L9h9+fuCFg==", + "node_modules/@react-types/accordion": { + "version": "3.0.0-alpha.19", + "resolved": "https://registry.npmjs.org/@react-types/accordion/-/accordion-3.0.0-alpha.19.tgz", + "integrity": "sha512-WJaitKz56zRKUwBqDM4OOKtmIdD0lr5nruWoM2IlGRO50WUzSFmAy/1aFiodAVZbun1v5IxbjST6/qSV4jPqug==", "dependencies": { - "@react-stately/utils": "^3.7.0", - "@react-types/checkbox": "^3.5.0", - "@react-types/shared": "^3.19.0", - "@swc/helpers": "^0.5.0" + "@react-types/shared": "^3.22.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/tooltip": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.4.3.tgz", - "integrity": "sha512-IX/XlLdwSQWy75TAOARm6hxajRWV0x/C7vGA54O+JNvvfZ212+nxVyTSduM+zjULzhOPICSSUFKmX4ZCV/aHSg==", + "node_modules/@react-types/breadcrumbs": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.4.tgz", + "integrity": "sha512-gQPLi71i+4zE6m5S74v7bpZ/yBERtlUt5qBcvB4C7gJu8aR4cFrv1YFZ//9f8uwlAHjau7XBpVlbBDlhfb2aOQ==", "dependencies": { - "@react-stately/overlays": "^3.6.1", - "@react-stately/utils": "^3.7.0", - "@react-types/tooltip": "^3.4.3", - "@swc/helpers": "^0.5.0" + "@react-types/link": "^3.5.4", + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/tree": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.7.1.tgz", - "integrity": "sha512-D0BWcLTRx7EOTdAJCgYV6zm18xpNDxmv4meKJ/WmYSFq1bkHPN75NLv7VPf5Uvsm66xshbO/B3A4HB2/ag1yPA==", - "dependencies": { - "@react-stately/collections": "^3.10.0", - "@react-stately/selection": "^3.13.3", - "@react-stately/utils": "^3.7.0", - "@react-types/shared": "^3.19.0", - "@swc/helpers": "^0.5.0" + "node_modules/@react-types/button": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.9.3.tgz", + "integrity": "sha512-YHlSeH85FhasJXOmkY4x+6If74ZpUh88C2fMlw0HUA/Bq/KGckUoriV8cnMqSnB1OwPqi8dpBZGfFVj6f6lh9A==", + "dependencies": { + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/utils": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.7.0.tgz", - "integrity": "sha512-VbApRiUV2rhozOfk0Qj9xt0qjVbQfLTgAzXLdrfeZSBnyIgo1bFRnjDpnDZKZUUCeGQcJJI03I9niaUtY+kwJQ==", + "node_modules/@react-types/calendar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.4.1.tgz", + "integrity": "sha512-tiCkHi6IQtYcVoAESG79eUBWDXoo8NImo+Mj8WAWpo1lOA3SV1W2PpeXkoRNqtloilQ0aYcmsaJJUhciQG4ndg==", "dependencies": { - "@swc/helpers": "^0.5.0" + "@internationalized/date": "^3.5.0", + "@react-types/shared": "^3.21.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-stately/virtualizer": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@react-stately/virtualizer/-/virtualizer-3.6.1.tgz", - "integrity": "sha512-Gq5gQ1YPgTakPCkWnmp9P6p5uGoVS+phm6Ie34lmZQ+E62lrkHK0XG0bkOuvMSdWwzql0oLg03E/SMOahI9vNA==", + "node_modules/@react-types/checkbox": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.8.0.tgz", + "integrity": "sha512-IBJ2bAsb3xoXaL+f0pwfRLDvRkhxfcX/q4NRJ2oT9jeHLU+j6svgK1Dqk8IGmY+vw1ltKbbMlIVeVonKQ3fgHw==", "dependencies": { - "@react-aria/utils": "^3.19.0", - "@react-types/shared": "^3.19.0", - "@swc/helpers": "^0.5.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-types/accordion": { - "version": "3.0.0-alpha.15", - "resolved": "https://registry.npmjs.org/@react-types/accordion/-/accordion-3.0.0-alpha.15.tgz", - "integrity": "sha512-BzR/9zVS1plc7s22szg5q2l15q+2pyyiM7S87Jfs9ROduM9GJjS3MwFvUyXAaYbh9t0Wkw+3ZZITUENimwFVPA==", + "node_modules/@react-types/combobox": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.11.0.tgz", + "integrity": "sha512-L6EEcIUIk7lsVvhO1Z1bklgH5bM84fBht03TC+es9YvS2T1Z9hdtyjBFcH6b3lVW9RwAArdUTL82/RNtvgD0Eg==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-types/button": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.7.4.tgz", - "integrity": "sha512-y1JOnJ3pqg2ezZz/fdwMMToPj+8fgj/He7z1NRWtIy1/I7HP+ilSK6S/MLO2jRsM2QfCq8KSw5MQEZBPiPWsjw==", + "node_modules/@react-types/datepicker": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.7.3.tgz", + "integrity": "sha512-SpA91itY03QaBvTAGP4X62SEAOoKJr91Av/U5DgH8gP7Ev4Ui+I3Aqh+w8Qw6nxKX4aAvDUx6wEHwLQLbvJUPA==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@internationalized/date": "^3.5.3", + "@react-types/calendar": "^3.4.5", + "@react-types/overlays": "^3.8.6", + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-types/checkbox": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.5.0.tgz", - "integrity": "sha512-fCisTdqFKkz7FvxNoexXIiVsTBt0ZwIyeIZz/S41M6hzIZM38nKbh6yS/lveQ+/877Dn7+ngvbpJ8QYnXYVrIQ==", + "node_modules/@react-types/datepicker/node_modules/@react-types/calendar": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.4.5.tgz", + "integrity": "sha512-FAAUbqe8iPiNf/OtdxnpOuAEJzyeRgfK2QCzfb4BIVnNNaTDkbxGCI5wrqHfBQ4FASECJeNlkjYXtbvijaooyw==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@internationalized/date": "^3.5.3", + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/dialog": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.4.tgz", - "integrity": "sha512-WCEkUf93XauGaPaF1efTJ8u04Z5iUgmmzRbFnGLrske7rQJYfryP3+26zCxtKKlOTgeFORq5AHeH6vqaMKOhhg==", + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.9.tgz", + "integrity": "sha512-8r9P1b1gq/cUv2bTPPNL3IFVEj9R5sIPACoSXznXkpXxh5FLU6yUPHDeQjvmM50q7KlEOgrPYhGl5pW525kLww==", "dependencies": { - "@react-types/overlays": "^3.8.1", - "@react-types/shared": "^3.19.0" + "@react-types/overlays": "^3.8.6", + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/grid": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.0.tgz", - "integrity": "sha512-ZIzFDbuBgqaPNvZ18/fOdm9Ol0m5rFPlhSxQfyAgUOXFaQhl/1+BsG8FsHla/Y6tTmxDt5cVrF5PX2CWzZmtOw==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.5.tgz", + "integrity": "sha512-kvE3Y+i0/RGLrf8qn/uVK1nVxXygNf5Jm6h9S6UdZkEVsclcqHKIX8UzqQgEUTd99jMHZk7fbKPm/La8uJ9yFQ==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-types/label": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/@react-types/label/-/label-3.7.5.tgz", - "integrity": "sha512-iNO5T1UYK7FPF23cwRLQJ4zth2rqoJWbz27Wikwt8Cw8VbVVzfLBPUBZoUyeBVZ0/zzTvEgZUW75OrmKb4gqhw==", + "node_modules/@react-types/link": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.5.4.tgz", + "integrity": "sha512-5hVAlKE4wiEVHmkqQG9/G4sdar257CISmLzWh9xf8heq14a93MBIHm7S9mhHULk2a84EC9bNoTi8Hh6P6nnMEw==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, - "node_modules/@react-types/link": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.4.4.tgz", - "integrity": "sha512-/FnKf7W6nCNZ2E96Yo1gaX63eSxERmtovQbkRRdsgPLfgRcqzQIVzQtNJThIbVNncOnAw3qvIyhrS0weUTFacQ==", + "node_modules/@react-types/listbox": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.4.8.tgz", + "integrity": "sha512-HNLBvyhR02p8GaZsW8hAu4YwkDjaG/rcuCT/l4Sdxzsm7szPlFMEVBZ9Ji3Ffzj+9P20OgFJ+VylWs7EkUwJAA==", "dependencies": { - "@react-aria/interactions": "^3.17.0", - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/menu": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.9.3.tgz", - "integrity": "sha512-0dgIIM9z3hzjFltT+1/L8Hj3oDEcdYkexQhaA+jv6xBHUI5Bqs4SaJAeSGrGz5u6tsrHBPEgf/TLk9Dg9c7XMA==", + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.9.8.tgz", + "integrity": "sha512-nkRCsfD3NXsJOv6mAnXCFyH2eGOFsmOOJOBQeOl9dj7BcdX9dcqp2PzUWPl33GrY9rYcXiRx4wsbUoqO1KVU4g==", "dependencies": { - "@react-types/overlays": "^3.8.1", - "@react-types/shared": "^3.19.0" + "@react-types/overlays": "^3.8.6", + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/overlays": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.1.tgz", - "integrity": "sha512-aDI/K3E2XACkey8SCBmAerLhYSUFa8g8tML4SoQbfEJPRj+jJztbHbg9F7b3HKDUk4ZOjcUdQRfz1nFHORdbtQ==", + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.6.tgz", + "integrity": "sha512-7xBuroYqwADppt7IRGfM8lbxVwlZrhMtTzeIdUot595cqFdRlpd/XAo2sRnEeIjYW9OSI8I5v4kt3AG7bdCQlg==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/progress": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.4.2.tgz", - "integrity": "sha512-UvnBt1OtjgQgOM3556KpuAXSdvSIVGSeD4+otTfkl05ieTcy6Lx7ef3TFI2KfQP45a9JeRBstTNpThBmuRe03A==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.5.3.tgz", + "integrity": "sha512-IcICNYRPFHQxl6iXi5jDgSZ3I9k2UQ2rIFcnoGo43K0hekv6fRdbbXWJU9ndShs3OfCHTPHEV5ooYB3UujNOAQ==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/radio": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.5.0.tgz", - "integrity": "sha512-jpAG03eYxLvD1+zLoHXVUR7BCXfzbaQnOv5vu2R4EXhBA7t1/HBOAY/WHbUEgrnyDYa2na7dr/RbY81H9JqR0g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.8.0.tgz", + "integrity": "sha512-0gvG74lgiaRo0DO46hoB5NxGFXhq5DsHaPZcCcb9VZ8cCzZMrO7U/B3JhF82TI2DndSx/AoiAMOQsc0v4ZwiGg==", + "dependencies": { + "@react-types/shared": "^3.23.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/select": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-types/select/-/select-3.9.3.tgz", + "integrity": "sha512-hK5RvA6frMbLdynRkegNW1lMOD0l9aFsW9X8WuTAg0zV6iZouU0hhSCT6JRDefJrv+m0X3fRdohMuVNZOhlA1g==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/shared": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.19.0.tgz", - "integrity": "sha512-h852l8bWhqUxbXIG8vH3ab7gE19nnP3U1kuWf6SNSMvgmqjiRN9jXKPIFxF/PbfdvnXXm0yZSgSMWfUCARF0Cg==", + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.0.tgz", + "integrity": "sha512-GQm/iPiii3ikcaMNR4WdVkJ4w0mKtV3mLqeSfSqzdqbPr6vONkqXbh3RhPlPmAJs1b4QHnexd/wZQP3U9DHOwQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/slider": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.7.2.tgz", + "integrity": "sha512-HvC/Mdt/z741xcU0ymeNxslnowQ5EAHOSzyf2JMgXmle+pEIbbepz5QUVaOmEveQHS3bjxE/+n2yBTKbxP8CJg==", + "dependencies": { + "@react-types/shared": "^3.23.0" + }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/switch": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.4.0.tgz", - "integrity": "sha512-vUA4Etm7ZiThYN3IotPXl99gHYZNJlc/f9o/SgAUSxtk5pBv5unOSmXLdrvk01Kd6TJ/MjL42IxRShygyr8mTQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.2.tgz", + "integrity": "sha512-4i35eZ5GtVDgu9KFhlyLyXanspcQp5WEnPyaBKn3pDRDcpzAL7yNP/Rwqc/JDdcJWngV080o7loJCgEfJ6UFaQ==", "dependencies": { - "@react-types/checkbox": "^3.5.0", - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/table": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.8.0.tgz", - "integrity": "sha512-/7IBG4ZlJHvEPQwND/q6ZFzfXq0Bc1ohaocDFzEOeNtVUrgQ2rFS64EY2p8G7BL9XDJFTY2R5dLYqjyGFojUvQ==", + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.9.4.tgz", + "integrity": "sha512-31EI0KAHwX7TbgERLBLVuD3nvpZUo0Wie7S7FEARmirIRfzm1fIkdDk5hfIHry2Lp4mq2/aqXLCY+oDR+lC2pw==", "dependencies": { - "@react-types/grid": "^3.2.0", - "@react-types/shared": "^3.19.0" + "@react-types/grid": "^3.2.5", + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/tabs": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.1.tgz", - "integrity": "sha512-vPxSbLCU7RT+Rupvu/1uOAesxlR/53GD5ZbgLuQRr/oEZRbsjY8Cs3CE3LGv49VdvBWivXUvHiF5wSE7CdWs1w==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.6.tgz", + "integrity": "sha512-ubvB7pB4+e5OpIuYR1CYip53iW9rJRIWvioHTYfcX0DnMabEcVP6Ymdqr5bDh/VsBEhiddsNgMduQwJm6bUTew==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/textfield": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.7.3.tgz", - "integrity": "sha512-M2u9NK3iqQEmTp4G1Dk36pCleyH/w1n+N52u5n0fRlxvucY/Od8W1zvk3w9uqJLFHSlzleHsfSvkaETDJn7FYw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.9.2.tgz", + "integrity": "sha512-8UcabahYhKm3KTu9CQBhz745FioUWO6CWgYusBpxMDJ+HnlhCC2JWyQvqg5tT98sr5AeSek4Jt/XS3ovzrhCDg==", "dependencies": { - "@react-types/shared": "^3.19.0" + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@react-types/tooltip": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.4.3.tgz", - "integrity": "sha512-ne1SVhgofHRZNhoQM4iMCSjCstpdPBpM81B4KDJ7XmWax0+dP4qmdxMc7qvEm7GjuZLfYx5f44fWytKm1BkZmg==", + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.4.8.tgz", + "integrity": "sha512-6XVQ3cMaXVMif+F5PQCaVwxbgAL8HVRqVjt6DkHs8Xbae43hpEIwPrBYlWWMVpuZAcjXZLTGmmyPjYeORZZJ4A==", "dependencies": { - "@react-types/overlays": "^3.8.1", - "@react-types/shared": "^3.19.0" + "@react-types/overlays": "^3.8.6", + "@react-types/shared": "^3.23.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, "node_modules/@reactflow/background": { - "version": "11.3.8", - "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.8.tgz", - "integrity": "sha512-U4aI54F7PwqgYI0Knv72QFRI/wXeipPmIYAlDsA0j51+tlPxs3Nk2z7G1/4pC11GxEZkgQVfcIXro4l1Fk+bIQ==", + "version": "11.3.13", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.13.tgz", + "integrity": "sha512-hkvpVEhgvfTDyCvdlitw4ioKCYLaaiRXnuEG+1QM3Np+7N1DiWF1XOv5I8AFyNoJL07yXEkbECUTsHvkBvcG5A==", "dependencies": { - "@reactflow/core": "11.10.3", + "@reactflow/core": "11.11.3", "classcat": "^5.0.3", "zustand": "^4.4.1" }, @@ -2755,11 +3493,11 @@ } }, "node_modules/@reactflow/controls": { - "version": "11.2.8", - "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.8.tgz", - "integrity": "sha512-Y9YVx38sRjYtbPsI/xa+B1FGN73FV1GqqajlmGfrc1TmqhJaX+gaMXMvVazT/N5haK1hMJvOApUTLQ2V/5Rdbg==", + "version": "11.2.13", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.13.tgz", + "integrity": "sha512-3xgEg6ALIVkAQCS4NiBjb7ad8Cb3D8CtA7Vvl4Hf5Ar2PIVs6FOaeft9s2iDZGtsWP35ECDYId1rIFVhQL8r+A==", "dependencies": { - "@reactflow/core": "11.10.3", + "@reactflow/core": "11.11.3", "classcat": "^5.0.3", "zustand": "^4.4.1" }, @@ -2769,9 +3507,9 @@ } }, "node_modules/@reactflow/core": { - "version": "11.10.3", - "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.10.3.tgz", - "integrity": "sha512-nV3nep4fjBy3h8mYSnJcclGcQtj8fkUBmNkEwCZCK4ps+n3HNkXFB0BRisSnQz6GRQlYboSsi0cThEl3KdNITw==", + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.3.tgz", + "integrity": "sha512-+adHdUa7fJSEM93fWfjQwyWXeI92a1eLKwWbIstoCakHpL8UjzwhEh6sn+mN2h/59MlVI7Ehr1iGTt3MsfcIFA==", "dependencies": { "@types/d3": "^7.4.0", "@types/d3-drag": "^3.0.1", @@ -2789,11 +3527,11 @@ } }, "node_modules/@reactflow/minimap": { - "version": "11.7.8", - "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.8.tgz", - "integrity": "sha512-MwyP5q3VomC91Dhd4P6YcxEfnjDbREGYV6rRxbSJSTHiG0x7ETQCcPelYDGy7JvQej77Pa2yJ4g0FDwP7CsQEA==", + "version": "11.7.13", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.13.tgz", + "integrity": "sha512-m2MvdiGSyOu44LEcERDEl1Aj6x//UQRWo3HEAejNU4HQTlJnYrSN8tgrYF8TxC1+c/9UdyzQY5VYgrTwW4QWdg==", "dependencies": { - "@reactflow/core": "11.10.3", + "@reactflow/core": "11.11.3", "@types/d3-selection": "^3.0.3", "@types/d3-zoom": "^3.0.1", "classcat": "^5.0.3", @@ -2807,11 +3545,11 @@ } }, "node_modules/@reactflow/node-resizer": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.8.tgz", - "integrity": "sha512-u/EXLpvOfAmq1sGoPYwoX4gi0PnCi0mH3eHVClHNc8JQD0WCqcV1UeVV7H3PF+1SGhhg/aOv/vPG1PcQ5fu4jQ==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.13.tgz", + "integrity": "sha512-X7ceQ2s3jFLgbkg03n2RYr4hm3jTVrzkW2W/8ANv/SZfuVmF8XJxlERuD8Eka5voKqLda0ywIZGAbw9GoHLfUQ==", "dependencies": { - "@reactflow/core": "11.10.3", + "@reactflow/core": "11.11.3", "classcat": "^5.0.4", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", @@ -2823,11 +3561,11 @@ } }, "node_modules/@reactflow/node-toolbar": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.8.tgz", - "integrity": "sha512-cfvlTPeJa/ciQTosx2bGrjHT8K/UL9kznpvpOzeZFnJm5UQXmbwAYt4Vo6GfkD51mORcIL7ujQJvB9ym3ZI9KA==", + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.13.tgz", + "integrity": "sha512-aknvNICO10uWdthFSpgD6ctY/CTBeJUMV9co8T9Ilugr08Nb89IQ4uD0dPmr031ewMQxixtYIkw+sSDDzd2aaQ==", "dependencies": { - "@reactflow/core": "11.10.3", + "@reactflow/core": "11.11.3", "classcat": "^5.0.3", "zustand": "^4.4.1" }, @@ -2837,9 +3575,9 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", - "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", "dependencies": { "tslib": "^2.4.0" } @@ -2976,9 +3714,9 @@ } }, "node_modules/@types/d3-hierarchy": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz", - "integrity": "sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==" + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", @@ -2989,9 +3727,9 @@ } }, "node_modules/@types/d3-path": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.2.tgz", - "integrity": "sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" }, "node_modules/@types/d3-polygon": { "version": "3.0.2", @@ -3067,14 +3805,27 @@ } }, "node_modules/@types/geojson": { - "version": "7946.0.13", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.13.tgz", - "integrity": "sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==" + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" }, "node_modules/@types/google.maps": { - "version": "3.53.5", - "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.53.5.tgz", - "integrity": "sha512-HoRq4Te8J6krH7hj+TfdYepqegoKZCj3kkaK5gf+ySFSHLvyqYkDvkrtbcVJXQ6QBphQ0h1TF7p4J6sOh4r/zg==" + "version": "3.55.2", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.55.2.tgz", + "integrity": "sha512-JcTwzkxskR8DN/nnX96Pie3gGN3WHiPpuxzuQ9z3516o1bB243d8w8DHUJ8BohuzoT1o3HUFta2ns/mkZC8KRw==" + }, + "node_modules/@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" + }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dependencies": { + "@types/lodash": "*" + } }, "node_modules/@types/node": { "version": "20.5.0", @@ -3082,14 +3833,14 @@ "integrity": "sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==" }, "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { "version": "18.2.20", @@ -3109,26 +3860,29 @@ "@types/react": "*" } }, - "node_modules/@types/react-is": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz", - "integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", - "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", "dependencies": { "@types/react": "*" } }, "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } }, "node_modules/ansi-styles": { "version": "3.2.1", @@ -3141,19 +3895,6 @@ "node": ">=4" } }, - "node_modules/ansi-styles/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/ansi-styles/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -3232,20 +3973,22 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -3260,9 +4003,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -3278,10 +4021,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -3318,9 +4061,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001521", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001521.tgz", - "integrity": "sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==", + "version": "1.0.30001615", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz", + "integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==", "funding": [ { "type": "opencollective", @@ -3358,15 +4101,9 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3379,6 +4116,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -3395,9 +4135,9 @@ } }, "node_modules/classcat": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz", - "integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==" + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==" }, "node_modules/client-only": { "version": "0.0.1", @@ -3405,9 +4145,9 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } @@ -3425,20 +4165,17 @@ } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "color-name": "1.1.3" } }, "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/color-string": { "version": "1.9.1", @@ -3449,10 +4186,26 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/color2k": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.2.tgz", - "integrity": "sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz", + "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==" }, "node_modules/commander": { "version": "4.1.1", @@ -3463,14 +4216,9 @@ } }, "node_modules/compute-scroll-into-view": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz", - "integrity": "sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" }, "node_modules/convert-source-map": { "version": "1.9.0", @@ -3500,12 +4248,17 @@ "node": ">=10" } }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, "engines": { - "node": ">= 6" + "node": ">= 8" } }, "node_modules/cssesc": { @@ -3520,9 +4273,9 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/d3-color": { "version": "3.1.0", @@ -3652,10 +4405,20 @@ "csstype": "^3.0.2" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/electron-to-chromium": { - "version": "1.4.495", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.495.tgz", - "integrity": "sha512-mwknuemBZnoOCths4GtpU/SDuVMp3uQHKa2UNJT9/aVD6WVRjGpXOxRGX7lm6ILIenTdGXPSTCTDaWos5tEU8Q==" + "version": "1.4.754", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.754.tgz", + "integrity": "sha512-7Kr5jUdns5rL/M9wFFmMZAgFDuL2YOnanFH4OI4iFzUqyh3XOL7nAGbSlSMZdzKMIyyTpNSbqZsWG9odwLeKvA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/error-ex": { "version": "1.3.2", @@ -3665,15 +4428,10 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -3695,9 +4453,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3721,9 +4479,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dependencies": { "reusify": "^1.0.4" } @@ -3752,22 +4510,37 @@ "flat": "cli.js" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "engines": { "node": "*" }, "funding": { "type": "patreon", - "url": "https://www.patreon.com/infusion" + "url": "https://github.com/sponsors/rawify" } }, "node_modules/framer-motion": { - "version": "10.16.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.0.tgz", - "integrity": "sha512-R+88Mkr/1dr7XHjacwptfJyrywRzQ1HZX3YSZtN4tFMBq1O8GGCbDEv31Nf/H08o0hUXLC87GkxsR/1bZgwXfw==", + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.18.0.tgz", + "integrity": "sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==", "dependencies": { "tslib": "^2.4.0" }, @@ -3787,15 +4560,25 @@ } } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/framer-motion/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "optional": true, "os": [ @@ -3806,9 +4589,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-nonce": { "version": "1.0.1", @@ -3819,19 +4605,21 @@ } }, "node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3858,17 +4646,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -3877,6 +4654,17 @@ "node": ">=4" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -3905,28 +4693,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, "node_modules/intl-messageformat": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.0.tgz", - "integrity": "sha512-AvojYuOaRb6r2veOKfTVpxH9TrmjSdc5iR9R5RgBwrDZYSmAAFVT+QLbW3C4V7Qsg0OguMp67Q/EoUkxZzXRGw==", + "version": "10.5.11", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.11.tgz", + "integrity": "sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", + "@formatjs/ecma402-abstract": "1.18.2", "@formatjs/fast-memoize": "2.2.0", - "@formatjs/icu-messageformat-parser": "2.6.0", + "@formatjs/icu-messageformat-parser": "2.7.6", "tslib": "^2.4.0" } }, @@ -3939,9 +4713,9 @@ } }, "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -3955,11 +4729,11 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3973,6 +4747,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3992,18 +4774,40 @@ "node": ">=0.12.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz", - "integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "bin": { "jiti": "bin/jiti.js" } }, "node_modules/jose": { - "version": "4.14.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", - "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -4045,6 +4849,11 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, "node_modules/lodash.foreach": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", @@ -4113,14 +4922,25 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/mz": { @@ -4134,9 +4954,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -4151,35 +4971,34 @@ } }, "node_modules/next": { - "version": "13.4.17", - "resolved": "https://registry.npmjs.org/next/-/next-13.4.17.tgz", - "integrity": "sha512-f0L+lbQA+GFkHu9wpupiURLFIEEPSVQhUuR+5lQNI+aFzbCbCGl7h0Vurs1jA4wtP7T7fEO0iSWmt37+88wIZA==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz", + "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==", "dependencies": { - "@next/env": "13.4.17", - "@swc/helpers": "0.5.1", + "@next/env": "13.5.6", + "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", - "postcss": "8.4.14", + "postcss": "8.4.31", "styled-jsx": "5.1.1", - "watchpack": "2.4.0", - "zod": "3.21.4" + "watchpack": "2.4.0" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=16.8.0" + "node": ">=16.14.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.4.17", - "@next/swc-darwin-x64": "13.4.17", - "@next/swc-linux-arm64-gnu": "13.4.17", - "@next/swc-linux-arm64-musl": "13.4.17", - "@next/swc-linux-x64-gnu": "13.4.17", - "@next/swc-linux-x64-musl": "13.4.17", - "@next/swc-win32-arm64-msvc": "13.4.17", - "@next/swc-win32-ia32-msvc": "13.4.17", - "@next/swc-win32-x64-msvc": "13.4.17" + "@next/swc-darwin-arm64": "13.5.6", + "@next/swc-darwin-x64": "13.5.6", + "@next/swc-linux-arm64-gnu": "13.5.6", + "@next/swc-linux-arm64-musl": "13.5.6", + "@next/swc-linux-x64-gnu": "13.5.6", + "@next/swc-linux-x64-musl": "13.5.6", + "@next/swc-win32-arm64-msvc": "13.5.6", + "@next/swc-win32-ia32-msvc": "13.5.6", + "@next/swc-win32-x64-msvc": "13.5.6" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -4197,14 +5016,14 @@ } }, "node_modules/next-auth": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.23.1.tgz", - "integrity": "sha512-mL083z8KgRtlrIV6CDca2H1kduWJuK/3pTS0Fe2og15KOm4v2kkLGdSDfc2g+019aEBrJUT0pPW2Xx42ImN1WA==", + "version": "4.24.7", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.7.tgz", + "integrity": "sha512-iChjE8ov/1K/z98gdKbn2Jw+2vLgJtVV39X+rCP5SGnVQuco7QOr19FRNGMIrD8d3LYhHWV9j9sKLzq1aDWWQQ==", "dependencies": { "@babel/runtime": "^7.20.13", "@panva/hkdf": "^1.0.2", "cookie": "^0.5.0", - "jose": "^4.11.4", + "jose": "^4.15.5", "oauth": "^0.9.15", "openid-client": "^5.4.0", "preact": "^10.6.3", @@ -4212,7 +5031,7 @@ "uuid": "^8.3.2" }, "peerDependencies": { - "next": "^12.2.5 || ^13", + "next": "^12.2.5 || ^13 || ^14", "nodemailer": "^6.6.5", "react": "^17.0.2 || ^18", "react-dom": "^17.0.2 || ^18" @@ -4223,10 +5042,18 @@ } } }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/next/node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -4235,10 +5062,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -4247,9 +5078,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -4281,9 +5112,9 @@ } }, "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "engines": { "node": ">= 6" } @@ -4296,20 +5127,12 @@ "node": "^10.13.0 || >=12.0.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/openid-client": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.3.tgz", - "integrity": "sha512-sVQOvjsT/sbSfYsQI/9liWQGVZH/Pp3rrtlGEwgk/bbHfrUDZ24DN57lAagIwFtuEu+FM9Ev7r85s8S/yPjimQ==", + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", + "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", "dependencies": { - "jose": "^4.14.4", + "jose": "^4.15.5", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -4318,14 +5141,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/openid-client/node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "engines": { - "node": ">= 6" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4354,12 +5169,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/path-parse": { @@ -4367,6 +5182,29 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4469,20 +5307,26 @@ } }, "node_modules/postcss-load-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" }, "engines": { "node": ">= 14" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" @@ -4496,6 +5340,28 @@ } } }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/postcss-nested": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", @@ -4515,9 +5381,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4532,9 +5398,9 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/preact": { - "version": "10.17.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.17.0.tgz", - "integrity": "sha512-SNsI8cbaCcUS5tbv9nlXuCfIXnJ9ysBMWk0WnB6UWwcVA3qZ2O6FxqDFECMAMttvLQcW/HaNZUe2BLidyvrVYw==", + "version": "10.21.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.21.0.tgz", + "integrity": "sha512-aQAIxtzWEwH8ou+OovWVSVNlFImL7xUCwJX3YMqA3U8iKCNC34999fFOnWjYNsylgfPgMexpbk7WYOLtKr/mxg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -4591,9 +5457,9 @@ ] }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -4602,36 +5468,36 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-icons": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", - "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", "peerDependencies": { "react": "*" } }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/react-remove-scroll": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz", - "integrity": "sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg==", + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.9.tgz", + "integrity": "sha512-bvHCLBrFfM2OgcrpPY2YW84sPdS2o2HKWJUf1xGyGLnSoEnOTOBpahIarjRuYtN0ryahCeP242yf+5TrBX/pZA==", "dependencies": { - "react-remove-scroll-bar": "^2.3.4", + "react-remove-scroll-bar": "^2.3.6", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", @@ -4651,9 +5517,9 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", "dependencies": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -4694,9 +5560,9 @@ } }, "node_modules/react-textarea-autosize": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.2.tgz", - "integrity": "sha512-uOkyjkEl0ByEK21eCJMHDGBAAd/BoFQBawYK5XItjAmCTeSbjxghd8qnt7nzsLYzidjnoObu6M26xts0YGKsGg==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", + "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==", "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", @@ -4725,16 +5591,16 @@ } }, "node_modules/reactflow": { - "version": "11.10.3", - "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.10.3.tgz", - "integrity": "sha512-DGNrTdkWjZtPOhj5MV8fiWWGkJo+otMVdoJ9l67bQL+Xf+8NkJ4AHmRXoYIxtgcENzwTr5WTAIJlswV9i91cyw==", + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.3.tgz", + "integrity": "sha512-wusd1Xpn1wgsSEv7UIa4NNraCwH9syBtubBy4xVNXg3b+CDKM+sFaF3hnMx0tr0et4km9urIDdNvwm34QiZong==", "dependencies": { - "@reactflow/background": "11.3.8", - "@reactflow/controls": "11.2.8", - "@reactflow/core": "11.10.3", - "@reactflow/minimap": "11.7.8", - "@reactflow/node-resizer": "2.2.8", - "@reactflow/node-toolbar": "1.3.8" + "@reactflow/background": "11.3.13", + "@reactflow/controls": "11.2.13", + "@reactflow/core": "11.11.3", + "@reactflow/minimap": "11.7.13", + "@reactflow/node-resizer": "2.2.13", + "@reactflow/node-toolbar": "1.3.13" }, "peerDependencies": { "react": ">=17", @@ -4761,14 +5627,14 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4821,9 +5687,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } @@ -4836,6 +5702,36 @@ "compute-scroll-into-view": "^3.0.2" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -4844,6 +5740,11 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -4853,9 +5754,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -4868,6 +5769,94 @@ "node": ">=10.0.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -4896,13 +5885,13 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "node_modules/sucrase": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", - "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", - "glob": "7.1.6", + "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", @@ -4913,7 +5902,7 @@ "sucrase-node": "bin/sucrase-node" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/supercluster": { @@ -4956,11 +5945,11 @@ } }, "node_modules/tailwind-variants": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.1.13.tgz", - "integrity": "sha512-G2M7M74hjq0nAo6QdEfUJgF+0t9DecFUw91GC1P9YTnwMcfB3uChT5U5e2DuNU42xoOz15lzo7r0mPdMzZkylg==", + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.1.20.tgz", + "integrity": "sha512-AMh7x313t/V+eTySKB0Dal08RHY7ggYK0MSn/ad8wKWOrDUIzyiWNayRUm2PIJ4VRkvRnfNuyRuKbLV3EN+ewQ==", "dependencies": { - "tailwind-merge": "^1.13.2" + "tailwind-merge": "^1.14.0" }, "engines": { "node": ">=16.x", @@ -4971,19 +5960,19 @@ } }, "node_modules/tailwindcss": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", - "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.18.2", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -5006,6 +5995,14 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -5050,9 +6047,9 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/tslib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/typescript": { "version": "5.1.6", @@ -5067,9 +6064,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz", + "integrity": "sha512-JixKH8GR2pWYshIPUg/NujK3JO7JiqEEUiNArE86NQyrgUuZeTlZQN3xuS/yiV5Kb48ev9K6RqNkaJjXsdg7Jw==", "funding": [ { "type": "opencollective", @@ -5085,7 +6082,7 @@ } ], "dependencies": { - "escalade": "^3.1.1", + "escalade": "^3.1.2", "picocolors": "^1.0.0" }, "bin": { @@ -5096,9 +6093,9 @@ } }, "node_modules/use-callback-ref": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", - "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", "dependencies": { "tslib": "^2.0.0" }, @@ -5206,10 +6203,130 @@ "node": ">=10.13.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, "node_modules/yallist": { "version": "4.0.0", @@ -5217,25 +6334,17 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "engines": { - "node": ">= 14" - } - }, - "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "node": ">= 6" } }, "node_modules/zustand": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.0.tgz", - "integrity": "sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", "dependencies": { "use-sync-external-store": "1.2.0" }, diff --git a/docs/samples/opera/package.json b/docs/samples/opera/package.json index 515599387..3199e0add 100644 --- a/docs/samples/opera/package.json +++ b/docs/samples/opera/package.json @@ -14,6 +14,7 @@ "@mui/lab": "^5.0.0-alpha.141", "@mui/material": "^5.14.6", "@nextui-org/react": "^2.0.22", + "@nextui-org/theme": "^2.2.3", "@react-google-maps/api": "^2.19.2", "@types/node": "20.5.0", "@types/react": "18.2.20", @@ -27,7 +28,7 @@ "react-dom": "^18.2.0", "react-icons": "^4.10.1", "reactflow": "^11.10.3", - "tailwindcss": "3.3.3", + "tailwindcss": "^3.3.3", "typescript": "5.1.6" } } From c810af0a855b0926f426c8e21fd0103943e5c204 Mon Sep 17 00:00:00 2001 From: Haishi2016 Date: Fri, 26 Apr 2024 04:17:44 +0200 Subject: [PATCH 07/26] remove Docker check; remove 'v' prefix of version parameter (#228) * remove Docker check; remove 'v' prefix of version parameter * remove v prefix for powershell --- cli/cmd/up.go | 7 ++++--- cli/install/install.ps1 | 2 +- cli/install/install.sh | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cli/cmd/up.go b/cli/cmd/up.go index 7c9b95b1f..4313e6b0e 100644 --- a/cli/cmd/up.go +++ b/cli/cmd/up.go @@ -70,9 +70,10 @@ var UpCmd = &cobra.Command{ return } } else { - if !handleDocker() { - return - } + // we don't need to check for Docker, as we are not using it + // if !handleDocker() { + // return + // } if !handleKubectl() { return } diff --git a/cli/install/install.ps1 b/cli/install/install.ps1 index a9b3e8936..f0d65e1d2 100644 --- a/cli/install/install.ps1 +++ b/cli/install/install.ps1 @@ -84,7 +84,7 @@ function GetVersionInfo { $release = $Releases | Where-Object { $_.tag_name -notlike "*rc*" } | Select-Object -First 1 } else { - $release = $Releases | Where-Object { $_.tag_name -eq "v$Version" } | Select-Object -First 1 + $release = $Releases | Where-Object { $_.tag_name -eq "$Version" } | Select-Object -First 1 } return $release diff --git a/cli/install/install.sh b/cli/install/install.sh index 45d1dc0f8..9b4401df3 100644 --- a/cli/install/install.sh +++ b/cli/install/install.sh @@ -196,7 +196,7 @@ if [ -z "$1" ]; then echo "Getting the latest Maestro CLI..." getLatestRelease else - retVal=v$1 + retVal=$1 fi verifySupported $retVal From b2cf925517f857eb6ec312ff136a2c427330f5d5 Mon Sep 17 00:00:00 2001 From: Jiawei Du <59427055+msftcoderdjw@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:43:11 +0800 Subject: [PATCH 08/26] [K8S] Merge ADO changes to OSS (#233) * Merge ADO K8S changes to OSS * honor OSS changes * expose delete sync delay for futuer override from helm charts * generate helm charts * fix typo in InstanceStatus * add debug logs for k8s target provider * refactor webhook and add metrics * refactor reconciliation policy to meet API spec * watch operationId change instead of annnotation change * webhook fixes * fix typo * refine reconciliation policy * refactor doc * remove version property in target/intance since we will have new version implementation * fix typo --- .../managers/solution/solution-manager.go | 2 - api/pkg/apis/v1alpha1/model/instance.go | 2 - api/pkg/apis/v1alpha1/model/target.go | 2 - .../apis/v1alpha1/providers/target/k8s/k8s.go | 2 + .../build_deployment/standalone.md | 31 - .../build_deployment/symphony_mode.md | 60 ++ docs/symphony-book/images/k8s-reconciler.png | Bin 0 -> 158372 bytes docs/symphony-book/managers/_overview.md | 2 +- k8s/apis/ai/v1/model_webhook.go | 52 +- k8s/apis/ai/v1/skill_webhook.go | 55 +- k8s/apis/ai/v1/webhook_suite_test.go | 20 +- k8s/apis/fabric/v1/configutil_test.go | 3 +- k8s/apis/fabric/v1/device_webhook.go | 55 +- k8s/apis/fabric/v1/target_types.go | 28 +- k8s/apis/fabric/v1/target_webhook.go | 181 +++-- k8s/apis/fabric/v1/webhook_suite_test.go | 20 +- k8s/apis/fabric/v1/zz_generated.deepcopy.go | 24 - k8s/apis/federation/v1/catalog_types.go | 3 +- k8s/apis/federation/v1/catalog_webhook.go | 49 +- k8s/apis/metrics/v1/attributes.go | 20 + k8s/apis/metrics/v1/metrics.go | 84 +++ k8s/apis/model/v1/common_types.go | 74 +- k8s/apis/model/v1/zz_generated.deepcopy.go | 115 +++ k8s/apis/solution/v1/instance_types.go | 27 +- k8s/apis/solution/v1/instance_webhook.go | 134 +++- k8s/apis/solution/v1/solution_types.go | 3 +- k8s/apis/solution/v1/webhook_suite_test.go | 20 +- k8s/apis/solution/v1/zz_generated.deepcopy.go | 24 - k8s/apis/workflow/v1/activation_types.go | 3 +- k8s/apis/workflow/v1/campaign_types.go | 3 +- .../crd/bases/fabric.symphony_targets.yaml | 19 +- .../crd/bases/federation.symphony_sites.yaml | 2 - .../bases/solution.symphony_instances.yaml | 20 +- .../bases/workflow.symphony_activations.yaml | 1 - k8s/configutils/configutil.go | 3 +- k8s/constants/constants.go | 39 +- k8s/constants/eula.txt | 20 + k8s/controllers/ai/suite_test.go | 12 +- k8s/controllers/fabric/suite_test.go | 146 +++- k8s/controllers/fabric/target_controller.go | 325 +++------ .../fabric/target_controller_test.go | 240 ++++++ k8s/controllers/metrics/attributes.go | 24 + k8s/controllers/metrics/metrics.go | 104 +++ .../solution/instance_controller.go | 415 +++++------ .../solution/instance_controller_test.go | 287 ++++++++ k8s/controllers/solution/suite_test.go | 146 +++- k8s/main.go | 183 ++++- k8s/predicates/operationid.go | 42 ++ k8s/reconcilers/delete_test.go | 363 +++++++++ k8s/reconcilers/deployment.go | 689 ++++++++++++++++++ k8s/reconcilers/options.go | 82 +++ k8s/reconcilers/policies_test.go | 544 ++++++++++++++ k8s/reconcilers/reconciler.go | 30 + k8s/reconcilers/suite_test.go | 21 + k8s/reconcilers/update_test.go | 375 ++++++++++ k8s/testing/mocks.go | 383 ++++++++++ k8s/utils/helper.go | 44 +- k8s/utils/model/provisioning-states.go | 34 + k8s/utils/models/provisioning-states.go | 18 - k8s/utils/symphony-api.go | 241 ++++++ k8s/utils/symphony-api_test.go | 80 ++ .../helm/symphony/templates/symphony.yaml | 42 +- 62 files changed, 5221 insertions(+), 851 deletions(-) delete mode 100644 docs/symphony-book/build_deployment/standalone.md create mode 100644 docs/symphony-book/build_deployment/symphony_mode.md create mode 100644 docs/symphony-book/images/k8s-reconciler.png create mode 100644 k8s/apis/metrics/v1/attributes.go create mode 100644 k8s/apis/metrics/v1/metrics.go create mode 100644 k8s/constants/eula.txt create mode 100644 k8s/controllers/fabric/target_controller_test.go create mode 100644 k8s/controllers/metrics/attributes.go create mode 100644 k8s/controllers/metrics/metrics.go create mode 100644 k8s/controllers/solution/instance_controller_test.go create mode 100644 k8s/predicates/operationid.go create mode 100644 k8s/reconcilers/delete_test.go create mode 100644 k8s/reconcilers/deployment.go create mode 100644 k8s/reconcilers/options.go create mode 100644 k8s/reconcilers/policies_test.go create mode 100644 k8s/reconcilers/reconciler.go create mode 100644 k8s/reconcilers/suite_test.go create mode 100644 k8s/reconcilers/update_test.go create mode 100644 k8s/testing/mocks.go create mode 100644 k8s/utils/model/provisioning-states.go delete mode 100644 k8s/utils/models/provisioning-states.go create mode 100644 k8s/utils/symphony-api.go create mode 100644 k8s/utils/symphony-api_test.go diff --git a/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go b/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go index d782ad0bb..f3ac65442 100644 --- a/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go +++ b/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go @@ -467,8 +467,6 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy }) //} - summary.IsRemoval = remove - successCount := 0 for _, v := range targetResult { successCount += v diff --git a/api/pkg/apis/v1alpha1/model/instance.go b/api/pkg/apis/v1alpha1/model/instance.go index 4b6389882..fce9f3c90 100644 --- a/api/pkg/apis/v1alpha1/model/instance.go +++ b/api/pkg/apis/v1alpha1/model/instance.go @@ -34,8 +34,6 @@ type ( Pipelines []PipelineSpec `json:"pipelines,omitempty"` Arguments map[string]map[string]string `json:"arguments,omitempty"` Generation string `json:"generation,omitempty"` - // Defines the version of a particular resource - Version string `json:"version,omitempty"` } // TargertRefSpec defines the target the instance will deploy to diff --git a/api/pkg/apis/v1alpha1/model/target.go b/api/pkg/apis/v1alpha1/model/target.go index 919880336..245451400 100644 --- a/api/pkg/apis/v1alpha1/model/target.go +++ b/api/pkg/apis/v1alpha1/model/target.go @@ -31,8 +31,6 @@ type ( Topologies []TopologySpec `json:"topologies,omitempty"` ForceRedeploy bool `json:"forceRedeploy,omitempty"` Generation string `json:"generation,omitempty"` - // Defines the version of a particular resource - Version string `json:"version,omitempty"` } ) diff --git a/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go b/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go index 85c0aab81..b8c6c734f 100644 --- a/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go +++ b/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go @@ -803,6 +803,8 @@ func deploymentToComponents(deployment v1.Deployment) ([]model.ComponentSpec, er } } } + componentsJson, _ := json.Marshal(components) + log.Debugf(" P (K8s Target Provider): deploymentToComponents - components: %s", string(componentsJson)) return components, nil } func convertComponentSpecToSidecar(c model.ComponentSpec) model.SidecarSpec { diff --git a/docs/symphony-book/build_deployment/standalone.md b/docs/symphony-book/build_deployment/standalone.md deleted file mode 100644 index 436b6f296..000000000 --- a/docs/symphony-book/build_deployment/standalone.md +++ /dev/null @@ -1,31 +0,0 @@ -# Run Symphony in standalone mode - -_(last edit: 6/27/2023)_ - -Symphony typically runs as a Kubernetes controller that operates Symphony CRDs. When running as a Kubernetes controller, Symphony delegates all state management of these CRDs to Kubernetes, and it uses webhooks to invoke Symphony API when the state of these objects change. So, Symphony Kubernetes controller is a very thin layer hooking up to Kubernetes API server, while all Symphony business logic resides behind Symphony API. Using the same architecture, we allow Symphony to run natively as an [Azure resource provider](https://learn.microsoft.com/azure/azure-resource-manager/management/resource-providers-and-types) that forwards external resource operations (such as update and remove) to Symphony API, as shown in the following diagram. - -![Architecture](../images/architecture.png) - -However, when running in a standalone mode, Symphony takes over object state management by itself. Symphony allows object state to be stored in any supported data storage via a state provider and it runs a state reconciliation loop that periodically generates a reconciliation job. Symphony listens to these jobs and invokes corresponding APIs for state reconciliation. This separation of job creation and job execution allows Symphony to handle both automatic reconciliation as well as on-demand reconciliation using the same job manager. - -## State reconciliation flows - -Under Kubernetes mode, users interact with Symphony Kubernetes CRDs using Kubernetes APIs and tools. Symphony controller queries Symphony API (via `/queue GET`) to decide if a new reconciliation job needs to be queued. If so, it queues a new job through the `/queue POST` route. The queued job is forward to a event bus, to which a [job vendor](../vendors/job.md) subscribes. When a reconcile job is received, the job vendor generates a Symphony deployment spec and calls the `/reconcile` route of a solution vendor. - -When running under the standalone mode, Symphony REST API queues reconciliation jobs when it updates Symphony objects. In addition, the job vendor is configured to periodically trigger reconciliation events by itself at configured intervals. - -![no-k8s](../images/no-k8s.png) - -## Launch Symphony in standalone mode - -Symphony runs as a single process in standalone mode. To launch Symphony in standalone mode, simply launch the `symphony-api` binary with a configuration file and an optional tracing level switch (such as `Error`, `Info` and `Debug`): - -```bash -./symphony-api -c ~/symphony-api-no-k8s.json -l Debug -``` - -Once launched, you can access Symphony's [REST API](../api/_overview.md) using any web clients. - -## Next steps - -* [Quick start - launch a Redis container with standalone Symphony](../get-started/deploy_redis_no_k8s.md) \ No newline at end of file diff --git a/docs/symphony-book/build_deployment/symphony_mode.md b/docs/symphony-book/build_deployment/symphony_mode.md new file mode 100644 index 000000000..30b343a4e --- /dev/null +++ b/docs/symphony-book/build_deployment/symphony_mode.md @@ -0,0 +1,60 @@ +# Symphony Launch Mode + +_(last edit: 4/25/2024)_ + +Symphony typically runs as a Kubernetes controller that operates Symphony CRDs. When running as a Kubernetes controller, Symphony delegates all state management of these CRDs to Kubernetes, and it integrates with Kubernetes reconciliation loop, uses webhooks to invoke Symphony API when the state of these objects change. So, Symphony Kubernetes controller is a very thin layer hooking up to Kubernetes API server, while all Symphony business logic resides behind Symphony API. Using the same architecture, we allow Symphony to run natively as an [Azure resource provider](https://learn.microsoft.com/azure/azure-resource-manager/management/resource-providers-and-types) that forwards external resource operations (such as update and remove) to Symphony API, as shown in the following diagram. + +![Architecture](../images/architecture.png) + +However, when running in a standalone mode, Symphony takes over object state management by itself. Symphony allows object state to be stored in any supported data storage via a state provider and it runs a state reconciliation loop that periodically generates a reconciliation job. Symphony listens to these jobs and invokes corresponding APIs for state reconciliation. This separation of job creation and job execution allows Symphony to handle both automatic reconciliation as well as on-demand reconciliation using the same job manager. + +## State reconciliation flows + +### Kubernetes mode + +Under Kubernetes mode, users interact with Symphony Kubernetes CRDs using Kubernetes APIs and tools. Symphony controller queries Symphony API (via `/queue GET`), calculates the deployment spec, and check its parity, then decide if a new reconciliation job needs to be queued. + +If requeue is needed, Symphony controller queues a new deployment job through the `/queue POST` route. The queued job is forward to a event bus, to which a [job vendor](../vendors/job.md) subscribes. When a reconcile job is received, the job vendor uses the Symphony deployment spec generated in Symphony controller and calls the `/reconcile` route of a solution vendor. + +And Symphony controller exposes fine-grained reonciliation policy control to let users decide how to reconciliate when current spec is converged to termination state. + +In target and instance k8s spec, users can define an optional property - reconciliationPolicy. + +```yaml +spec: + reconciliationPolicy: + state: + interval: +``` + +By default, Symphony controller will use periodic reconciliation policy. If the caller doesn't set `reconciliationPolicy`, or doesn't explicitly specify `reconciliationPolicy.interval`, the default interval will be `30m`. + +The caller can disable the periodic reconciliation policy by setting `reconciliationPolicy.interval = 0` or `reconciliationPolicy.state = 'inactive'`. + +Whenever an instance or a target is changed, The generation is bumped by the kubernetes API server which in turn triggers a reconciliation on the controllers. We then go through the following steps, + +![img](../images/k8s-reconciler.png) + +### Standalone mode + +When running under the standalone mode, Symphony REST API queues reconciliation jobs when it updates Symphony objects. In addition, the job vendor is configured to periodically trigger reconciliation events by itself at configured intervals. + +Once the job vendor subscribes the new reconciliation event, it will generate the deployment spec and calls the `/reconcile` route of a solution vendor. + +And under standalone mode, the reconciliation policy is not configured on individual resource, which is different from Kubernetes mode. + +![no-k8s](../images/no-k8s.png) + +## Launch Symphony in standalone mode + +Symphony runs as a single process in standalone mode. To launch Symphony in standalone mode, simply launch the `symphony-api` binary with a configuration file and an optional tracing level switch (such as `Error`, `Info` and `Debug`): + +```bash +./symphony-api -c ~/symphony-api-no-k8s.json -l Debug +``` + +Once launched, you can access Symphony's [REST API](../api/_overview.md) using any web clients. + +## Next steps + +* [Quick start - launch a Redis container with standalone Symphony](../get-started/deploy_redis_no_k8s.md) \ No newline at end of file diff --git a/docs/symphony-book/images/k8s-reconciler.png b/docs/symphony-book/images/k8s-reconciler.png new file mode 100644 index 0000000000000000000000000000000000000000..a35df9793d1edf3afb79cbe8814737c6c814286f GIT binary patch literal 158372 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%0A^o=-0pU7e(GbMZ4zvIfVbhChE;je&i`;(B! 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

VLgg}i`We^h59J>Bw!R5= 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�&8$2-xK@g*-)-NI2Fr<|*ZEF6wB2IUB_7sF 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 I*7(8y0Z8D~s{jB1 literal 0 HcmV?d00001 diff --git a/docs/symphony-book/managers/_overview.md b/docs/symphony-book/managers/_overview.md index 6c818d178..ceb1d6d67 100644 --- a/docs/symphony-book/managers/_overview.md +++ b/docs/symphony-book/managers/_overview.md @@ -17,7 +17,7 @@ In Symphony's [HB-MVP pattern](https://www.linkedin.com/pulse/hb-mvp-design-patt These managers implement CRUD operations on corresponding object types, and they are hosted by corresponding vendors such as the devices vendor and target vendor. These vendors collectively offer Symphony REST API to manage Symphony objects. - When hosted on Kubernetes, such object operations are delegated to Kubernetes API. In such a case, users interact with Symphony objects through native Kubernetes API instead of through these REST API routes. For an example, see [Run Symphony in standalone mode](../build_deployment/standalone.md). + When hosted on Kubernetes, such object operations are delegated to Kubernetes API. In such a case, users interact with Symphony objects through native Kubernetes API instead of through these REST API routes. For an example, see [Run Symphony in kubernetes mode](../build_deployment/symphony_mode.md). * Solution manager diff --git a/k8s/apis/ai/v1/model_webhook.go b/k8s/apis/ai/v1/model_webhook.go index eb233d9e8..9adb85543 100644 --- a/k8s/apis/ai/v1/model_webhook.go +++ b/k8s/apis/ai/v1/model_webhook.go @@ -9,6 +9,7 @@ package v1 import ( "context" "strings" + "time" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -19,6 +20,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" configv1 "gopls-workspace/apis/config/v1" + "gopls-workspace/apis/metrics/v1" configutils "gopls-workspace/configutils" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -28,6 +30,7 @@ import ( var modellog = logf.Log.WithName("model-resource") var myModelClient client.Client var modelValidationPolicies []configv1.ValidationPolicy +var modelWebhookValidationMetrics *metrics.Metrics func (r *Model) SetupWebhookWithManager(mgr ctrl.Manager) error { myModelClient = mgr.GetClient() @@ -41,6 +44,15 @@ func (r *Model) SetupWebhookWithManager(mgr ctrl.Manager) error { modelValidationPolicies = v } + // initialize the controller operation metrics + if modelWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + modelWebhookValidationMetrics = metrics + } + return ctrl.NewWebhookManagedBy(mgr). For(r). Complete() @@ -67,14 +79,50 @@ var _ webhook.Validator = &Model{} func (r *Model) ValidateCreate() error { modellog.Info("validate create", "name", r.Name) - return r.validateCreateModel() + validateCreateTime := time.Now() + validationError := r.validateCreateModel() + if validationError != nil { + modelWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.ModelResourceType, + ) + } else { + modelWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.ModelResourceType, + ) + } + + return validationError } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type func (r *Model) ValidateUpdate(old runtime.Object) error { modellog.Info("validate update", "name", r.Name) - return r.validateUpdateModel() + validateUpdateTime := time.Now() + validationError := r.validateUpdateModel() + if validationError != nil { + modelWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.InvalidResource, + metrics.ModelResourceType, + ) + } else { + modelWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.ValidResource, + metrics.ModelResourceType, + ) + } + + return validationError } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type diff --git a/k8s/apis/ai/v1/skill_webhook.go b/k8s/apis/ai/v1/skill_webhook.go index 5c55fa818..dcd791ef3 100644 --- a/k8s/apis/ai/v1/skill_webhook.go +++ b/k8s/apis/ai/v1/skill_webhook.go @@ -9,6 +9,8 @@ package v1 import ( "context" "fmt" + "gopls-workspace/apis/metrics/v1" + "time" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -20,6 +22,7 @@ import ( // log is for logging in this package. var skilllog = logf.Log.WithName("skill-resource") var mySkillClient client.Client +var skillWebhookValidationMetrics *metrics.Metrics func (r *Skill) SetupWebhookWithManager(mgr ctrl.Manager) error { mySkillClient = mgr.GetClient() @@ -27,6 +30,16 @@ func (r *Skill) SetupWebhookWithManager(mgr ctrl.Manager) error { skill := rawObj.(*Skill) return []string{skill.Spec.DisplayName} }) + + // initialize the controller operation metrics + if skillWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + skillWebhookValidationMetrics = metrics + } + return ctrl.NewWebhookManagedBy(mgr). For(r). Complete() @@ -53,14 +66,52 @@ var _ webhook.Validator = &Skill{} func (r *Skill) ValidateCreate() error { skilllog.Info("validate create", "name", r.Name) - return r.validateCreateSkill() + validateCreateTime := time.Now() + validationError := r.validateCreateSkill() + + if validationError != nil { + skillWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.SkillResourceType, + ) + } else { + skillWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.SkillResourceType, + ) + } + + return validationError } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type func (r *Skill) ValidateUpdate(old runtime.Object) error { skilllog.Info("validate update", "name", r.Name) - return r.validateUpdateSkill() + validateUpdateTime := time.Now() + validationError := r.validateUpdateSkill() + + if validationError != nil { + skillWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.InvalidResource, + metrics.SkillResourceType, + ) + } else { + skillWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.ValidResource, + metrics.SkillResourceType, + ) + } + + return validationError } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type diff --git a/k8s/apis/ai/v1/webhook_suite_test.go b/k8s/apis/ai/v1/webhook_suite_test.go index 4d1d38f20..73a83df5c 100644 --- a/k8s/apis/ai/v1/webhook_suite_test.go +++ b/k8s/apis/ai/v1/webhook_suite_test.go @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ -package v1 +package v1_test import ( "context" @@ -20,6 +20,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + api "gopls-workspace/apis/ai/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" //+kubebuilder:scaffold:imports "k8s.io/apimachinery/pkg/runtime" @@ -41,24 +43,22 @@ var ctx context.Context var cancel context.CancelFunc func TestAPIs(t *testing.T) { - t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) RunGinkgoSpecs(t, "Webhook Suite") } var _ = BeforeSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "oss", "crd", "bases")}, ErrorIfCRDPathMissing: false, WebhookInstallOptions: envtest.WebhookInstallOptions{ - Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + Paths: []string{filepath.Join("..", "..", "..", "config", "oss", "webhook")}, }, } @@ -67,14 +67,13 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) scheme := runtime.NewScheme() - err = AddToScheme(scheme) + err = api.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) err = admissionv1beta1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) - err = admissionv1beta1.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) + //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) Expect(err).NotTo(HaveOccurred()) @@ -92,10 +91,10 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&Model{}).SetupWebhookWithManager(mgr) + err = (&api.Model{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) - err = (&Skill{}).SetupWebhookWithManager(mgr) + err = (&api.Skill{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:webhook @@ -121,7 +120,6 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") cancel() By("tearing down the test environment") err := testEnv.Stop() diff --git a/k8s/apis/fabric/v1/configutil_test.go b/k8s/apis/fabric/v1/configutil_test.go index b991e5fb8..ab89023b9 100644 --- a/k8s/apis/fabric/v1/configutil_test.go +++ b/k8s/apis/fabric/v1/configutil_test.go @@ -12,8 +12,9 @@ import ( configv1 "gopls-workspace/apis/config/v1" configutils "gopls-workspace/configutils" + k8smodel "gopls-workspace/apis/model/v1" + apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" - k8smodel "github.com/eclipse-symphony/symphony/k8s/apis/model/v1" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/k8s/apis/fabric/v1/device_webhook.go b/k8s/apis/fabric/v1/device_webhook.go index ef9bc493f..b884e2bd4 100644 --- a/k8s/apis/fabric/v1/device_webhook.go +++ b/k8s/apis/fabric/v1/device_webhook.go @@ -9,6 +9,8 @@ package v1 import ( "context" "fmt" + "gopls-workspace/apis/metrics/v1" + "time" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -20,6 +22,7 @@ import ( // log is for logging in this package. var devicelog = logf.Log.WithName("device-resource") var myDeviceClient client.Client +var deviceWebhookValidationMetrics *metrics.Metrics func (r *Device) SetupWebhookWithManager(mgr ctrl.Manager) error { myDeviceClient = mgr.GetClient() @@ -28,6 +31,16 @@ func (r *Device) SetupWebhookWithManager(mgr ctrl.Manager) error { device := rawObj.(*Device) return []string{device.Spec.DisplayName} }) + + // initialize the controller operation metrics + if deviceWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + deviceWebhookValidationMetrics = metrics + } + return ctrl.NewWebhookManagedBy(mgr). For(r). Complete() @@ -54,14 +67,52 @@ var _ webhook.Validator = &Device{} func (r *Device) ValidateCreate() error { devicelog.Info("validate create", "name", r.Name) - return r.validateCreateDevice() + validateCreateTime := time.Now() + validationError := r.validateCreateDevice() + + if validationError != nil { + deviceWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.DeviceResourceType, + ) + } else { + deviceWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.DeviceResourceType, + ) + } + + return validationError } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type func (r *Device) ValidateUpdate(old runtime.Object) error { devicelog.Info("validate update", "name", r.Name) - return r.validateUpdateDevice() + validateUpdateTime := time.Now() + validationError := r.validateUpdateDevice() + + if validationError != nil { + deviceWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.InvalidResource, + metrics.DeviceResourceType, + ) + } else { + deviceWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.ValidResource, + metrics.DeviceResourceType, + ) + } + + return validationError } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type diff --git a/k8s/apis/fabric/v1/target_types.go b/k8s/apis/fabric/v1/target_types.go index 19e24ae29..563e7bc88 100644 --- a/k8s/apis/fabric/v1/target_types.go +++ b/k8s/apis/fabric/v1/target_types.go @@ -7,8 +7,8 @@ package v1 import ( - apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" - k8smodel "github.com/eclipse-symphony/symphony/k8s/apis/model/v1" + k8smodel "gopls-workspace/apis/model/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -16,14 +16,6 @@ import ( // +k8s:deepcopy-gen=false type ComponentProperties = runtime.RawExtension -// TargetStatus defines the observed state of Target -type TargetStatus struct { - // Important: Run "make" to regenerate code after modifying this file - Properties map[string]string `json:"properties,omitempty"` - ProvisioningStatus apimodel.ProvisioningStatus `json:"provisioningStatus"` - LastModified metav1.Time `json:"lastModified,omitempty"` -} - // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.properties.status` @@ -32,8 +24,8 @@ type Target struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec k8smodel.TargetSpec `json:"spec,omitempty"` - Status TargetStatus `json:"status,omitempty"` + Spec k8smodel.TargetSpec `json:"spec,omitempty"` + Status k8smodel.TargetStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true @@ -47,3 +39,15 @@ type TargetList struct { func init() { SchemeBuilder.Register(&Target{}, &TargetList{}) } + +func (i *Target) GetStatus() k8smodel.TargetStatus { + return i.Status +} + +func (i *Target) SetStatus(status k8smodel.TargetStatus) { + i.Status = status +} + +func (i *Target) GetReconciliationPolicy() *k8smodel.ReconciliationPolicySpec { + return i.Spec.ReconciliationPolicy +} diff --git a/k8s/apis/fabric/v1/target_webhook.go b/k8s/apis/fabric/v1/target_webhook.go index 602337d00..97ee931d5 100644 --- a/k8s/apis/fabric/v1/target_webhook.go +++ b/k8s/apis/fabric/v1/target_webhook.go @@ -9,8 +9,11 @@ package v1 import ( "context" "strings" + "time" configv1 "gopls-workspace/apis/config/v1" + "gopls-workspace/apis/metrics/v1" + v1 "gopls-workspace/apis/model/v1" configutils "gopls-workspace/configutils" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -27,11 +30,12 @@ import ( var targetlog = logf.Log.WithName("target-resource") var myTargetClient client.Client var targetValidationPolicies []configv1.ValidationPolicy +var targetWebhookValidationMetrics *metrics.Metrics func (r *Target) SetupWebhookWithManager(mgr ctrl.Manager) error { myTargetClient = mgr.GetClient() - mgr.GetFieldIndexer().IndexField(context.Background(), &Target{}, ".spec.displayName", func(rawObj client.Object) []string { + mgr.GetFieldIndexer().IndexField(context.Background(), &Target{}, "spec.displayName", func(rawObj client.Object) []string { target := rawObj.(*Target) return []string{target.Spec.DisplayName} }) @@ -41,6 +45,15 @@ func (r *Target) SetupWebhookWithManager(mgr ctrl.Manager) error { targetValidationPolicies = v } + // initialize the controller operation metrics + if targetWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + targetWebhookValidationMetrics = metrics + } + return ctrl.NewWebhookManagedBy(mgr). For(r). Complete() @@ -59,6 +72,14 @@ func (r *Target) Default() { if r.Spec.DisplayName == "" { r.Spec.DisplayName = r.ObjectMeta.Name } + + if r.Spec.Scope == "" { + r.Spec.Scope = "default" + } + + if r.Spec.ReconciliationPolicy != nil && r.Spec.ReconciliationPolicy.State == "" { + r.Spec.ReconciliationPolicy.State = v1.ReconciliationPolicy_Active + } } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. @@ -71,14 +92,50 @@ var _ webhook.Validator = &Target{} func (r *Target) ValidateCreate() error { targetlog.Info("validate create", "name", r.Name) - return r.validateCreateTarget() + validateCreateTime := time.Now() + validationError := r.validateCreateTarget() + if validationError != nil { + targetWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.TargetResourceType, + ) + } else { + targetWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.TargetResourceType, + ) + } + + return validationError } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type func (r *Target) ValidateUpdate(old runtime.Object) error { targetlog.Info("validate update", "name", r.Name) - return r.validateUpdateTarget() + validateUpdateTime := time.Now() + validationError := r.validateUpdateTarget() + if validationError != nil { + targetWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.InvalidResource, + metrics.TargetResourceType, + ) + } else { + targetWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.ValidResource, + metrics.TargetResourceType, + ) + } + + return validationError } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type @@ -91,71 +148,113 @@ func (r *Target) ValidateDelete() error { func (r *Target) validateCreateTarget() error { var allErrs field.ErrorList + + if err := r.validateUniqueNameOnCreate(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateValidationPolicy(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateReconciliationPolicy(); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) +} + +func (r *Target) validateUniqueNameOnCreate() *field.Error { var targets TargetList - err := myTargetClient.List(context.Background(), &targets, client.InNamespace(r.Namespace), client.MatchingFields{".spec.displayName": r.Spec.DisplayName}) + err := myTargetClient.List(context.Background(), &targets, client.InNamespace(r.Namespace), client.MatchingFields{"spec.displayName": r.Spec.DisplayName}) if err != nil { - allErrs = append(allErrs, field.InternalError(&field.Path{}, err)) - return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) + return field.InternalError(&field.Path{}, err) } + if len(targets.Items) != 0 { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("displayName"), r.Spec.DisplayName, "target display name is already taken")) - return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) - } - if len(targetValidationPolicies) > 0 { - err := myTargetClient.List(context.Background(), &targets, client.InNamespace(r.Namespace), &client.ListOptions{}) - if err != nil { - allErrs = append(allErrs, field.InternalError(&field.Path{}, err)) - return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) - } - for _, p := range targetValidationPolicies { - pack := extractTargetValidationPack(targets, p) - ret, err := configutils.CheckValidationPack(r.ObjectMeta.Name, readTargetValiationTarget(r, p), p.ValidationType, pack) - if err != nil { - return err - } - if ret != "" { - allErrs = append(allErrs, field.Forbidden(&field.Path{}, strings.ReplaceAll(p.Message, "%s", ret))) - return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) - } - } + return field.Invalid(field.NewPath("spec").Child("displayName"), r.Spec.DisplayName, "target display name is already taken") } + return nil } -func (r *Target) validateUpdateTarget() error { - var allErrs field.ErrorList +func (r *Target) validateUniqueNameOnUpdate() *field.Error { var targets TargetList - err := myTargetClient.List(context.Background(), &targets, client.InNamespace(r.Namespace), client.MatchingFields{".spec.displayName": r.Spec.DisplayName}) + err := myTargetClient.List(context.Background(), &targets, client.InNamespace(r.Namespace), client.MatchingFields{"spec.displayName": r.Spec.DisplayName}) if err != nil { - allErrs = append(allErrs, field.InternalError(&field.Path{}, err)) - return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) + return field.InternalError(&field.Path{}, err) } + if !(len(targets.Items) == 0 || len(targets.Items) == 1 && targets.Items[0].ObjectMeta.Name == r.ObjectMeta.Name) { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("displayName"), r.Spec.DisplayName, "target display name is already taken")) - return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) + return field.Invalid(field.NewPath("spec").Child("displayName"), r.Spec.DisplayName, "target display name is already taken") } + + return nil +} + +func (r *Target) validateValidationPolicy() *field.Error { + var targets TargetList if len(targetValidationPolicies) > 0 { - err = myTargetClient.List(context.Background(), &targets, client.InNamespace(r.Namespace), &client.ListOptions{}) + err := myTargetClient.List(context.Background(), &targets, client.InNamespace(r.Namespace), &client.ListOptions{}) if err != nil { - allErrs = append(allErrs, field.InternalError(&field.Path{}, err)) - return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) + return field.InternalError(&field.Path{}, err) } for _, p := range targetValidationPolicies { pack := extractTargetValidationPack(targets, p) - ret, err := configutils.CheckValidationPack(r.ObjectMeta.Name, readTargetValiationTarget(r, p), p.ValidationType, pack) + ret, err := configutils.CheckValidationPack(r.ObjectMeta.Name, readTargetValidationTarget(r, p), p.ValidationType, pack) if err != nil { - return err + return field.InternalError(&field.Path{}, err) } if ret != "" { - allErrs = append(allErrs, field.Forbidden(&field.Path{}, strings.ReplaceAll(p.Message, "%s", ret))) - return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) + return field.Forbidden(&field.Path{}, strings.ReplaceAll(p.Message, "%s", ret)) + } + } + } + return nil +} + +func (r *Target) validateReconciliationPolicy() *field.Error { + if r.Spec.ReconciliationPolicy != nil && r.Spec.ReconciliationPolicy.Interval != nil { + if duration, err := time.ParseDuration(*r.Spec.ReconciliationPolicy.Interval); err == nil { + if duration != 0 && duration < 1*time.Minute { + return field.Invalid(field.NewPath("spec").Child("reconciliationPolicy").Child("interval"), r.Spec.ReconciliationPolicy.Interval, "must be a non-negative value with a minimum of 1 minute, e.g. 1m") } + } else { + return field.Invalid(field.NewPath("spec").Child("reconciliationPolicy").Child("interval"), r.Spec.ReconciliationPolicy.Interval, "cannot be parsed as type of time.Duration") + } + } + + if r.Spec.ReconciliationPolicy != nil { + if !r.Spec.ReconciliationPolicy.State.IsActive() && !r.Spec.ReconciliationPolicy.State.IsInActive() { + return field.Invalid(field.NewPath("spec").Child("reconciliationPolicy").Child("state"), r.Spec.ReconciliationPolicy.State, "must be either 'active' or 'inactive'") } } + return nil } -func readTargetValiationTarget(target *Target, p configv1.ValidationPolicy) string { +func (r *Target) validateUpdateTarget() error { + var allErrs field.ErrorList + if err := r.validateUniqueNameOnUpdate(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateValidationPolicy(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateReconciliationPolicy(); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(schema.GroupKind{Group: "fabric.symphony", Kind: "Target"}, r.Name, allErrs) +} + +func readTargetValidationTarget(target *Target, p configv1.ValidationPolicy) string { if p.SelectorType == "topologies.bindings" && p.SelectorKey == "provider" { for _, topology := range target.Spec.Topologies { for _, binding := range topology.Bindings { diff --git a/k8s/apis/fabric/v1/webhook_suite_test.go b/k8s/apis/fabric/v1/webhook_suite_test.go index 011ea2d60..318d590b5 100644 --- a/k8s/apis/fabric/v1/webhook_suite_test.go +++ b/k8s/apis/fabric/v1/webhook_suite_test.go @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ -package v1 +package v1_test import ( "context" @@ -20,6 +20,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + api "gopls-workspace/apis/fabric/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" //+kubebuilder:scaffold:imports @@ -42,24 +44,22 @@ var ctx context.Context var cancel context.CancelFunc func TestAPIs(t *testing.T) { - t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) RunGinkgoSpecs(t, "Webhook Suite") } var _ = BeforeSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "oss", "crd", "bases")}, ErrorIfCRDPathMissing: false, WebhookInstallOptions: envtest.WebhookInstallOptions{ - Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + Paths: []string{filepath.Join("..", "..", "..", "config", "oss", "webhook")}, }, } @@ -68,10 +68,7 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) scheme := runtime.NewScheme() - err = AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = admissionv1beta1.AddToScheme(scheme) + err = api.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) err = admissionv1beta1.AddToScheme(scheme) @@ -95,10 +92,10 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&Device{}).SetupWebhookWithManager(mgr) + err = (&api.Device{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) - err = (&Target{}).SetupWebhookWithManager(mgr) + err = (&api.Target{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:webhook @@ -124,7 +121,6 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") cancel() By("tearing down the test environment") err := testEnv.Stop() diff --git a/k8s/apis/fabric/v1/zz_generated.deepcopy.go b/k8s/apis/fabric/v1/zz_generated.deepcopy.go index a48679086..71072473a 100644 --- a/k8s/apis/fabric/v1/zz_generated.deepcopy.go +++ b/k8s/apis/fabric/v1/zz_generated.deepcopy.go @@ -154,27 +154,3 @@ func (in *TargetList) DeepCopyObject() runtime.Object { } return nil } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TargetStatus) DeepCopyInto(out *TargetStatus) { - *out = *in - if in.Properties != nil { - in, out := &in.Properties, &out.Properties - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - in.ProvisioningStatus.DeepCopyInto(&out.ProvisioningStatus) - in.LastModified.DeepCopyInto(&out.LastModified) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetStatus. -func (in *TargetStatus) DeepCopy() *TargetStatus { - if in == nil { - return nil - } - out := new(TargetStatus) - in.DeepCopyInto(out) - return out -} diff --git a/k8s/apis/federation/v1/catalog_types.go b/k8s/apis/federation/v1/catalog_types.go index 1992131a2..4bd445713 100644 --- a/k8s/apis/federation/v1/catalog_types.go +++ b/k8s/apis/federation/v1/catalog_types.go @@ -7,7 +7,8 @@ package v1 import ( - k8smodel "github.com/eclipse-symphony/symphony/k8s/apis/model/v1" + k8smodel "gopls-workspace/apis/model/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/k8s/apis/federation/v1/catalog_webhook.go b/k8s/apis/federation/v1/catalog_webhook.go index b5efeb4ed..78675d327 100644 --- a/k8s/apis/federation/v1/catalog_webhook.go +++ b/k8s/apis/federation/v1/catalog_webhook.go @@ -9,6 +9,8 @@ package v1 import ( "context" "encoding/json" + "gopls-workspace/apis/metrics/v1" + "time" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" @@ -22,6 +24,7 @@ import ( // log is for logging in this package. var cataloglog = logf.Log.WithName("catalog-resource") var myCatalogClient client.Client +var catalogWebhookValidationMetrics *metrics.Metrics func (r *Catalog) SetupWebhookWithManager(mgr ctrl.Manager) error { myCatalogClient = mgr.GetClient() @@ -29,6 +32,16 @@ func (r *Catalog) SetupWebhookWithManager(mgr ctrl.Manager) error { target := rawObj.(*Catalog) return []string{target.Name} }) + + // initialize the controller operation metrics + if catalogWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + catalogWebhookValidationMetrics = metrics + } + return ctrl.NewWebhookManagedBy(mgr). For(r). Complete() @@ -55,14 +68,46 @@ var _ webhook.Validator = &Catalog{} func (r *Catalog) ValidateCreate() error { cataloglog.Info("validate create", "name", r.Name) - return r.validateCreateCatalog() + validateCreateTime := time.Now() + validationError := r.validateCreateCatalog() + if validationError != nil { + catalogWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.CatalogResourceType) + } else { + catalogWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.CatalogResourceType) + } + + return validationError } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type func (r *Catalog) ValidateUpdate(old runtime.Object) error { cataloglog.Info("validate update", "name", r.Name) - return r.validateUpdateCatalog() + validateUpdateTime := time.Now() + validationError := r.validateUpdateCatalog() + if validationError != nil { + catalogWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.InvalidResource, + metrics.CatalogResourceType) + } else { + catalogWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.ValidResource, + metrics.CatalogResourceType) + } + + return validationError } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type diff --git a/k8s/apis/metrics/v1/attributes.go b/k8s/apis/metrics/v1/attributes.go new file mode 100644 index 000000000..dabf27f5c --- /dev/null +++ b/k8s/apis/metrics/v1/attributes.go @@ -0,0 +1,20 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package metrics + +// Deployment gets common logging attributes for a deployment. +func Deployment( + validationType string, + validationResult string, + resourceType string, +) map[string]any { + return map[string]any{ + "validationType": validationType, + "validationResult": validationResult, + "resourceType": resourceType, + } +} diff --git a/k8s/apis/metrics/v1/metrics.go b/k8s/apis/metrics/v1/metrics.go new file mode 100644 index 000000000..6a87a7984 --- /dev/null +++ b/k8s/apis/metrics/v1/metrics.go @@ -0,0 +1,84 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package metrics + +import ( + "gopls-workspace/constants" + "time" + + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" +) + +const ( + //validation type + CreateOperationType string = "Create" + UpdateOperationType string = "Update" + //validation result + ValidResource string = "Valid" + InvalidResource string = "Invalid" + //resource type + TargetResourceType string = "Target" + InstanceResourceType string = "Instance" + CatalogResourceType string = "Catalog" + ModelResourceType string = "Model" + SkillResourceType string = "Skill" + DeviceResourceType string = "Device" +) + +// Metrics is a metrics tracker for a controller operation. +type Metrics struct { + controllerValidationLatency observability.Histogram +} + +func New() (*Metrics, error) { + observable := observability.New(constants.K8S) + + controllerValidationLatency, err := observable.Metrics.Histogram( + "symphony_controller_validation_latency", + "measure of overall controller validate latency", + ) + if err != nil { + return nil, err + } + + return &Metrics{ + controllerValidationLatency: controllerValidationLatency, + }, nil +} + +// Close closes all metrics. +func (m *Metrics) Close() { + if m == nil { + return + } +} + +// ControllerValidationLatency tracks the overall Controller validation latency. +func (m *Metrics) ControllerValidationLatency( + startTime time.Time, + validationType string, + validationResult string, + resourceType string, +) { + if m == nil { + return + } + + m.controllerValidationLatency.Add( + latency(startTime), + Deployment( + validationType, + validationResult, + resourceType, + ), + ) +} + +// Latency gets the time since the given start in milliseconds. +func latency(start time.Time) float64 { + return float64(time.Since(start)) / float64(time.Millisecond) +} diff --git a/k8s/apis/model/v1/common_types.go b/k8s/apis/model/v1/common_types.go index e7f72fade..c625a8c2a 100644 --- a/k8s/apis/model/v1/common_types.go +++ b/k8s/apis/model/v1/common_types.go @@ -7,10 +7,35 @@ package v1 import ( + "strings" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) +const ( + // ReconciliationPolicy_Active allows the controller to reconcile periodically + ReconciliationPolicy_Active ReconciliationPolicyState = "active" + // ReconciliationPolicy_Inactive disables periodic reconciliation + ReconciliationPolicy_Inactive ReconciliationPolicyState = "inactive" +) + +// +kubebuilder:validation:Enum=active;inactive; +type ReconciliationPolicyState string + +func (r ReconciliationPolicyState) String() string { + return string(r) +} + +func (r ReconciliationPolicyState) IsActive() bool { + return strings.ToLower(r.String()) == ReconciliationPolicy_Active.String() +} + +func (r ReconciliationPolicyState) IsInActive() bool { + return strings.ToLower(r.String()) == ReconciliationPolicy_Inactive.String() +} + // +kubebuilder:object:generate=true type SidecarSpec struct { Name string `json:"name,omitempty"` @@ -47,9 +72,32 @@ type TargetSpec struct { Constraints string `json:"constraints,omitempty"` Topologies []model.TopologySpec `json:"topologies,omitempty"` ForceRedeploy bool `json:"forceRedeploy,omitempty"` - // Defines the version of a particular resource - Version string `json:"version,omitempty"` - Generation string `json:"generation,omitempty"` + Generation string `json:"generation,omitempty"` + + // Optional ReconcilicationPolicy to specify how target controller should reconcile. + // Now only periodic reconciliation is supported. If the interval is 0, it will only reconcile + // when the instance is created or updated. + ReconciliationPolicy *ReconciliationPolicySpec `json:"reconciliationPolicy,omitempty"` +} + +// +kubebuilder:object:generate=true +type InstanceSpec struct { + Name string `json:"name"` + DisplayName string `json:"displayName,omitempty"` + Scope string `json:"scope,omitempty"` + Parameters map[string]string `json:"parameters,omitempty"` //TODO: Do we still need this? + Metadata map[string]string `json:"metadata,omitempty"` + Solution string `json:"solution"` + Target model.TargetSelector `json:"target,omitempty"` + Topologies []model.TopologySpec `json:"topologies,omitempty"` + Pipelines []model.PipelineSpec `json:"pipelines,omitempty"` + Arguments map[string]map[string]string `json:"arguments,omitempty"` + Generation string `json:"generation,omitempty"` + + // Optional ReconcilicationPolicy to specify how target controller should reconcile. + // Now only periodic reconciliation is supported. If the interval is 0, it will only reconcile + // when the instance is created or updated. + ReconciliationPolicy *ReconciliationPolicySpec `json:"reconciliationPolicy,omitempty"` } // +kubebuilder:object:generate=true @@ -116,3 +164,23 @@ type CatalogSpec struct { ObjectRef model.ObjectRef `json:"objectRef,omitempty"` Generation string `json:"generation,omitempty"` } + +// +kubebuilder:object:generate=true +type DeployableStatus struct { + Properties map[string]string `json:"properties,omitempty"` + ProvisioningStatus model.ProvisioningStatus `json:"provisioningStatus"` + LastModified metav1.Time `json:"lastModified,omitempty"` +} + +// InstanceStatus defines the observed state of Instance +type InstanceStatus = DeployableStatus + +// TargetStatus defines the observed state of Target +type TargetStatus = DeployableStatus + +// +kubebuilder:object:generate=true +type ReconciliationPolicySpec struct { + State ReconciliationPolicyState `json:"state"` + // +kubebuilder:validation:MinLength=1 + Interval *string `json:"interval,omitempty"` +} diff --git a/k8s/apis/model/v1/zz_generated.deepcopy.go b/k8s/apis/model/v1/zz_generated.deepcopy.go index 91bbe06ac..ba92cf76d 100644 --- a/k8s/apis/model/v1/zz_generated.deepcopy.go +++ b/k8s/apis/model/v1/zz_generated.deepcopy.go @@ -124,6 +124,116 @@ func (in *ComponentSpec) DeepCopy() *ComponentSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeployableStatus) DeepCopyInto(out *DeployableStatus) { + *out = *in + if in.Properties != nil { + in, out := &in.Properties, &out.Properties + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.ProvisioningStatus.DeepCopyInto(&out.ProvisioningStatus) + in.LastModified.DeepCopyInto(&out.LastModified) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployableStatus. +func (in *DeployableStatus) DeepCopy() *DeployableStatus { + if in == nil { + return nil + } + out := new(DeployableStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstanceSpec) DeepCopyInto(out *InstanceSpec) { + *out = *in + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Target.DeepCopyInto(&out.Target) + if in.Topologies != nil { + in, out := &in.Topologies, &out.Topologies + *out = make([]model.TopologySpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Pipelines != nil { + in, out := &in.Pipelines, &out.Pipelines + *out = make([]model.PipelineSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Arguments != nil { + in, out := &in.Arguments, &out.Arguments + *out = make(map[string]map[string]string, len(*in)) + for key, val := range *in { + var outVal map[string]string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + (*out)[key] = outVal + } + } + if in.ReconciliationPolicy != nil { + in, out := &in.ReconciliationPolicy, &out.ReconciliationPolicy + *out = new(ReconciliationPolicySpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceSpec. +func (in *InstanceSpec) DeepCopy() *InstanceSpec { + if in == nil { + return nil + } + out := new(InstanceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReconciliationPolicySpec) DeepCopyInto(out *ReconciliationPolicySpec) { + *out = *in + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconciliationPolicySpec. +func (in *ReconciliationPolicySpec) DeepCopy() *ReconciliationPolicySpec { + if in == nil { + return nil + } + out := new(ReconciliationPolicySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScheduleSpec) DeepCopyInto(out *ScheduleSpec) { *out = *in @@ -237,6 +347,11 @@ func (in *TargetSpec) DeepCopyInto(out *TargetSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ReconciliationPolicy != nil { + in, out := &in.ReconciliationPolicy, &out.ReconciliationPolicy + *out = new(ReconciliationPolicySpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSpec. diff --git a/k8s/apis/solution/v1/instance_types.go b/k8s/apis/solution/v1/instance_types.go index c2da622a9..f64fede6e 100644 --- a/k8s/apis/solution/v1/instance_types.go +++ b/k8s/apis/solution/v1/instance_types.go @@ -7,18 +7,11 @@ package v1 import ( - apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + k8smodel "gopls-workspace/apis/model/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// InstanceStatus defines the observed state of Instance -type InstanceStatus struct { - // Important: Run "make" to regenerate code after modifying this file - Properties map[string]string `json:"properties,omitempty"` - ProvisioningStatus apimodel.ProvisioningStatus `json:"provisioningStatus"` - LastModified metav1.Time `json:"lastModified,omitempty"` -} - // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.properties.status` @@ -30,8 +23,8 @@ type Instance struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec apimodel.InstanceSpec `json:"spec,omitempty"` - Status InstanceStatus `json:"status,omitempty"` + Spec k8smodel.InstanceSpec `json:"spec,omitempty"` + Status k8smodel.InstanceStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true @@ -45,3 +38,15 @@ type InstanceList struct { func init() { SchemeBuilder.Register(&Instance{}, &InstanceList{}) } + +func (i *Instance) GetStatus() k8smodel.InstanceStatus { + return i.Status +} + +func (i *Instance) SetStatus(status k8smodel.InstanceStatus) { + i.Status = status +} + +func (i *Instance) GetReconciliationPolicy() *k8smodel.ReconciliationPolicySpec { + return i.Spec.ReconciliationPolicy +} diff --git a/k8s/apis/solution/v1/instance_webhook.go b/k8s/apis/solution/v1/instance_webhook.go index 91b7b1db3..d004b0550 100644 --- a/k8s/apis/solution/v1/instance_webhook.go +++ b/k8s/apis/solution/v1/instance_webhook.go @@ -9,21 +9,29 @@ package v1 import ( "context" "fmt" + "gopls-workspace/apis/metrics/v1" + v1 "gopls-workspace/apis/model/v1" + "time" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" + + apierrors "k8s.io/apimachinery/pkg/api/errors" ) // log is for logging in this package. var instancelog = logf.Log.WithName("instance-resource") var myInstanceClient client.Client +var instanceWebhookValidationMetrics *metrics.Metrics func (r *Instance) SetupWebhookWithManager(mgr ctrl.Manager) error { myInstanceClient = mgr.GetClient() - mgr.GetFieldIndexer().IndexField(context.Background(), &Instance{}, ".spec.displayName", func(rawObj client.Object) []string { + mgr.GetFieldIndexer().IndexField(context.Background(), &Instance{}, "spec.displayName", func(rawObj client.Object) []string { target := rawObj.(*Instance) return []string{target.Spec.DisplayName} }) @@ -31,6 +39,16 @@ func (r *Instance) SetupWebhookWithManager(mgr ctrl.Manager) error { target := rawObj.(*Instance) return []string{target.Spec.Solution} }) + + // initialize the controller operation metrics + if instanceWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + instanceWebhookValidationMetrics = metrics + } + return ctrl.NewWebhookManagedBy(mgr). For(r). Complete() @@ -49,6 +67,10 @@ func (r *Instance) Default() { if r.Spec.DisplayName == "" { r.Spec.DisplayName = r.ObjectMeta.Name } + + if r.Spec.ReconciliationPolicy != nil && r.Spec.ReconciliationPolicy.State == "" { + r.Spec.ReconciliationPolicy.State = v1.ReconciliationPolicy_Active + } } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. @@ -61,14 +83,50 @@ var _ webhook.Validator = &Instance{} func (r *Instance) ValidateCreate() error { instancelog.Info("validate create", "name", r.Name) - return r.validateCreateInstance() + validateCreateTime := time.Now() + validationError := r.validateCreateInstance() + if validationError != nil { + instanceWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.InstanceResourceType, + ) + } else { + instanceWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.InstanceResourceType, + ) + } + + return validationError } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type func (r *Instance) ValidateUpdate(old runtime.Object) error { instancelog.Info("validate update", "name", r.Name) - return r.validateUpdateInstance() + validateUpdateTime := time.Now() + validationError := r.validateUpdateInstance() + if validationError != nil { + instanceWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.InvalidResource, + metrics.InstanceResourceType, + ) + } else { + instanceWebhookValidationMetrics.ControllerValidationLatency( + validateUpdateTime, + metrics.UpdateOperationType, + metrics.ValidResource, + metrics.InstanceResourceType, + ) + } + + return validationError } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type @@ -80,22 +138,80 @@ func (r *Instance) ValidateDelete() error { } func (r *Instance) validateCreateInstance() error { + var allErrs field.ErrorList + + if err := r.validateUniqueNameOnCreate(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateReconciliationPolicy(); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(schema.GroupKind{Group: "solution.symphony", Kind: "Instance"}, r.Name, allErrs) +} + +func (r *Instance) validateUpdateInstance() error { + var allErrs field.ErrorList + if err := r.validateUniqueNameOnUpdate(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateReconciliationPolicy(); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(schema.GroupKind{Group: "solution.symphony", Kind: "Instance"}, r.Name, allErrs) +} + +func (r *Instance) validateUniqueNameOnCreate() *field.Error { var instances InstanceList - myInstanceClient.List(context.Background(), &instances, client.InNamespace(r.Namespace), client.MatchingFields{".spec.displayName": r.Spec.DisplayName}) + err := myInstanceClient.List(context.Background(), &instances, client.InNamespace(r.Namespace), client.MatchingFields{"spec.displayName": r.Spec.DisplayName}) + if err != nil { + return field.InternalError(&field.Path{}, err) + } + if len(instances.Items) != 0 { - return fmt.Errorf("instance display name '%s' is already taken", r.Spec.DisplayName) + return field.Invalid(field.NewPath("spec").Child("displayName"), r.Spec.DisplayName, fmt.Sprintf("instance display name '%s' is already taken", r.Spec.DisplayName)) } return nil } -func (r *Instance) validateUpdateInstance() error { +func (r *Instance) validateUniqueNameOnUpdate() *field.Error { var instances InstanceList - err := myInstanceClient.List(context.Background(), &instances, client.InNamespace(r.Namespace), client.MatchingFields{".spec.displayName": r.Spec.DisplayName}) + err := myInstanceClient.List(context.Background(), &instances, client.InNamespace(r.Namespace), client.MatchingFields{"spec.displayName": r.Spec.DisplayName}) if err != nil { - return err + return field.InternalError(&field.Path{}, err) } + if !(len(instances.Items) == 0 || len(instances.Items) == 1 && instances.Items[0].ObjectMeta.Name == r.ObjectMeta.Name) { - return fmt.Errorf("instance display name '%s' is already taken", r.Spec.DisplayName) + return field.Invalid(field.NewPath("spec").Child("displayName"), r.Spec.DisplayName, fmt.Sprintf("instance display name '%s' is already taken", r.Spec.DisplayName)) } return nil } + +func (r *Instance) validateReconciliationPolicy() *field.Error { + if r.Spec.ReconciliationPolicy != nil && r.Spec.ReconciliationPolicy.Interval != nil { + if duration, err := time.ParseDuration(*r.Spec.ReconciliationPolicy.Interval); err == nil { + if duration != 0 && duration < 1*time.Minute { + return field.Invalid(field.NewPath("spec").Child("reconciliationPolicy").Child("interval"), r.Spec.ReconciliationPolicy.Interval, "must be a non-negative value with a minimum of 1 minute, e.g. 1m") + } + } else { + return field.Invalid(field.NewPath("spec").Child("reconciliationPolicy").Child("interval"), r.Spec.ReconciliationPolicy.Interval, "cannot be parsed as type of time.Duration") + } + } + + if r.Spec.ReconciliationPolicy != nil { + if !r.Spec.ReconciliationPolicy.State.IsActive() && !r.Spec.ReconciliationPolicy.State.IsInActive() { + return field.Invalid(field.NewPath("spec").Child("reconciliationPolicy").Child("state"), r.Spec.ReconciliationPolicy.State, "must be either 'active' or 'inactive'") + } + } + + return nil +} diff --git a/k8s/apis/solution/v1/solution_types.go b/k8s/apis/solution/v1/solution_types.go index 3698cc219..385d45e0f 100644 --- a/k8s/apis/solution/v1/solution_types.go +++ b/k8s/apis/solution/v1/solution_types.go @@ -7,7 +7,8 @@ package v1 import ( - k8smodel "github.com/eclipse-symphony/symphony/k8s/apis/model/v1" + k8smodel "gopls-workspace/apis/model/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/k8s/apis/solution/v1/webhook_suite_test.go b/k8s/apis/solution/v1/webhook_suite_test.go index a65399550..8f30db981 100644 --- a/k8s/apis/solution/v1/webhook_suite_test.go +++ b/k8s/apis/solution/v1/webhook_suite_test.go @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ -package v1 +package v1_test import ( "context" @@ -20,6 +20,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + api "gopls-workspace/apis/solution/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" //+kubebuilder:scaffold:imports "k8s.io/apimachinery/pkg/runtime" @@ -41,24 +43,22 @@ var ctx context.Context var cancel context.CancelFunc func TestAPIs(t *testing.T) { - t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) RunGinkgoSpecs(t, "Webhook Suite") } var _ = BeforeSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "oss", "crd", "bases")}, ErrorIfCRDPathMissing: false, WebhookInstallOptions: envtest.WebhookInstallOptions{ - Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + Paths: []string{filepath.Join("..", "..", "..", "config", "oss", "webhook")}, }, } @@ -67,10 +67,7 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) scheme := runtime.NewScheme() - err = AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = admissionv1beta1.AddToScheme(scheme) + err = api.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) err = admissionv1beta1.AddToScheme(scheme) @@ -94,10 +91,10 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&Solution{}).SetupWebhookWithManager(mgr) + err = (&api.Solution{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) - err = (&Instance{}).SetupWebhookWithManager(mgr) + err = (&api.Instance{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:webhook @@ -123,7 +120,6 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") cancel() By("tearing down the test environment") err := testEnv.Stop() diff --git a/k8s/apis/solution/v1/zz_generated.deepcopy.go b/k8s/apis/solution/v1/zz_generated.deepcopy.go index da7588cd6..2857134f2 100644 --- a/k8s/apis/solution/v1/zz_generated.deepcopy.go +++ b/k8s/apis/solution/v1/zz_generated.deepcopy.go @@ -74,30 +74,6 @@ func (in *InstanceList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *InstanceStatus) DeepCopyInto(out *InstanceStatus) { - *out = *in - if in.Properties != nil { - in, out := &in.Properties, &out.Properties - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - in.ProvisioningStatus.DeepCopyInto(&out.ProvisioningStatus) - in.LastModified.DeepCopyInto(&out.LastModified) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceStatus. -func (in *InstanceStatus) DeepCopy() *InstanceStatus { - if in == nil { - return nil - } - out := new(InstanceStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Solution) DeepCopyInto(out *Solution) { *out = *in diff --git a/k8s/apis/workflow/v1/activation_types.go b/k8s/apis/workflow/v1/activation_types.go index 563b84552..f4bcb0ef6 100644 --- a/k8s/apis/workflow/v1/activation_types.go +++ b/k8s/apis/workflow/v1/activation_types.go @@ -7,8 +7,9 @@ package v1 import ( + k8smodel "gopls-workspace/apis/model/v1" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" - k8smodel "github.com/eclipse-symphony/symphony/k8s/apis/model/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) diff --git a/k8s/apis/workflow/v1/campaign_types.go b/k8s/apis/workflow/v1/campaign_types.go index 42be150be..1ace718fc 100644 --- a/k8s/apis/workflow/v1/campaign_types.go +++ b/k8s/apis/workflow/v1/campaign_types.go @@ -7,7 +7,8 @@ package v1 import ( - k8smodel "github.com/eclipse-symphony/symphony/k8s/apis/model/v1" + k8smodel "gopls-workspace/apis/model/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/k8s/config/oss/crd/bases/fabric.symphony_targets.yaml b/k8s/config/oss/crd/bases/fabric.symphony_targets.yaml index 84fc7f2cf..e00885f17 100644 --- a/k8s/config/oss/crd/bases/fabric.symphony_targets.yaml +++ b/k8s/config/oss/crd/bases/fabric.symphony_targets.yaml @@ -129,6 +129,19 @@ spec: additionalProperties: type: string type: object + reconciliationPolicy: + description: Optional ReconcilicationPolicy to specify how target + controller should reconcile. Now only periodic reconciliation is + supported. If the interval is 0, it will only reconcile when the + instance is created or updated. + properties: + interval: + type: string + state: + type: string + required: + - state + type: object scope: type: string topologies: @@ -160,12 +173,8 @@ spec: type: object type: object type: array - version: - description: Defines the version of a particular resource - type: string type: object status: - description: TargetStatus defines the observed state of Target properties: lastModified: format: date-time @@ -173,8 +182,6 @@ spec: properties: additionalProperties: type: string - description: 'Important: Run "make" to regenerate code after modifying - this file' type: object provisioningStatus: description: Defines the state of the ARM resource for long running diff --git a/k8s/config/oss/crd/bases/federation.symphony_sites.yaml b/k8s/config/oss/crd/bases/federation.symphony_sites.yaml index 8c20597e4..3f1f146f5 100644 --- a/k8s/config/oss/crd/bases/federation.symphony_sites.yaml +++ b/k8s/config/oss/crd/bases/federation.symphony_sites.yaml @@ -53,7 +53,6 @@ spec: reason: type: string state: - description: State represents a response state type: integer type: object type: object @@ -67,7 +66,6 @@ spec: reason: type: string state: - description: State represents a response state type: integer type: object type: object diff --git a/k8s/config/oss/crd/bases/solution.symphony_instances.yaml b/k8s/config/oss/crd/bases/solution.symphony_instances.yaml index cd5e1c0a9..b204c564b 100644 --- a/k8s/config/oss/crd/bases/solution.symphony_instances.yaml +++ b/k8s/config/oss/crd/bases/solution.symphony_instances.yaml @@ -43,7 +43,6 @@ spec: metadata: type: object spec: - description: InstanceSpec defines the spec property of the InstanceState properties: arguments: additionalProperties: @@ -82,6 +81,19 @@ spec: - skill type: object type: array + reconciliationPolicy: + description: Optional ReconcilicationPolicy to specify how target + controller should reconcile. Now only periodic reconciliation is + supported. If the interval is 0, it will only reconcile when the + instance is created or updated. + properties: + interval: + type: string + state: + type: string + required: + - state + type: object scope: type: string solution: @@ -126,15 +138,11 @@ spec: type: object type: object type: array - version: - description: Defines the version of a particular resource - type: string required: - name - solution type: object status: - description: InstanceStatus defines the observed state of Instance properties: lastModified: format: date-time @@ -142,8 +150,6 @@ spec: properties: additionalProperties: type: string - description: 'Important: Run "make" to regenerate code after modifying - this file' type: object provisioningStatus: description: Defines the state of the ARM resource for long running diff --git a/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml b/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml index c7ac31fb5..c737018cd 100644 --- a/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml +++ b/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml @@ -69,7 +69,6 @@ spec: stage: type: string status: - description: State represents a response state type: integer updateTime: type: string diff --git a/k8s/configutils/configutil.go b/k8s/configutils/configutil.go index e89443cca..a050f58fa 100644 --- a/k8s/configutils/configutil.go +++ b/k8s/configutils/configutil.go @@ -12,6 +12,7 @@ import ( "os" configv1 "gopls-workspace/apis/config/v1" + "gopls-workspace/constants" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -21,7 +22,7 @@ import ( var ( namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" - configName = os.Getenv("CONFIG_NAME") + configName = os.Getenv(constants.ConfigName) ) func GetValidationPoilicies() (map[string][]configv1.ValidationPolicy, error) { diff --git a/k8s/constants/constants.go b/k8s/constants/constants.go index 39b2642a7..b1a76b858 100644 --- a/k8s/constants/constants.go +++ b/k8s/constants/constants.go @@ -9,26 +9,27 @@ package constants -const ( - AzureOperationKey = "management.azure.com/operationId" - EulaMessage = `MIT License - -Copyright (c) Microsoft Corporation. +import _ "embed" -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +const ( + FullGroupName = "symphony" + AzureOperationIdKey = "management.azure.com/operationId" + AzureCorrelationId = "management.azure.com/correlationId" + DefaultScope = "default" + K8S = "symphony-k8s" + OperationStartTimeKeyPostfix = FullGroupName + "/started-at" + FinalizerPostfix = FullGroupName + "/finalizer" +) -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +// Environment variables keys +const ( + SymphonyAPIUrlEnvName = "SYMPHONY_API_URL" + ConfigName = "CONFIG_NAME" + ApiCertEnvName = "API_SERVING_CA" +) -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE` +// Eula Message +var ( + //go:embed eula.txt + EulaMessage string ) diff --git a/k8s/constants/eula.txt b/k8s/constants/eula.txt new file mode 100644 index 000000000..be4cb860e --- /dev/null +++ b/k8s/constants/eula.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE \ No newline at end of file diff --git a/k8s/controllers/ai/suite_test.go b/k8s/controllers/ai/suite_test.go index 7df865041..c751736e7 100644 --- a/k8s/controllers/ai/suite_test.go +++ b/k8s/controllers/ai/suite_test.go @@ -4,12 +4,13 @@ * SPDX-License-Identifier: MIT */ -package ai +package ai_test import ( "path/filepath" "testing" + api "gopls-workspace/apis/ai/v1" . "gopls-workspace/testing" . "github.com/onsi/ginkgo/v2" @@ -21,8 +22,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - - aiv1 "gopls-workspace/apis/ai/v1" //+kubebuilder:scaffold:imports ) @@ -34,19 +33,17 @@ var k8sClient client.Client var testEnv *envtest.Environment func TestAPIs(t *testing.T) { - t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) RunGinkgoSpecs(t, "Controller Suite") } var _ = BeforeSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "oss", "crd", "bases")}, ErrorIfCRDPathMissing: true, } @@ -54,7 +51,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - err = aiv1.AddToScheme(scheme.Scheme) + err = api.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme @@ -66,7 +63,6 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") By("tearing down the test environment") err := testEnv.Stop() Expect(err).NotTo(HaveOccurred()) diff --git a/k8s/controllers/fabric/suite_test.go b/k8s/controllers/fabric/suite_test.go index eafa30123..d83da527a 100644 --- a/k8s/controllers/fabric/suite_test.go +++ b/k8s/controllers/fabric/suite_test.go @@ -4,17 +4,20 @@ * SPDX-License-Identifier: MIT */ -package fabric +package fabric_test import ( + "context" "path/filepath" "testing" + "time" . "gopls-workspace/testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - + "github.com/stretchr/testify/mock" + "go.uber.org/zap/zapcore" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -22,52 +25,117 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - fabricv1 "gopls-workspace/apis/fabric/v1" + api "gopls-workspace/apis/fabric/v1" + + controllers "gopls-workspace/controllers/fabric" + + ctrl "sigs.k8s.io/controller-runtime" //+kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment - func TestAPIs(t *testing.T) { - t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) RunGinkgoSpecs(t, "Controller Suite") } -var _ = BeforeSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - By("bootstrapping test environment") - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: true, - } - - cfg, err := testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - - err = fabricv1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - - //+kubebuilder:scaffold:scheme - - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient).NotTo(BeNil()) - -}) - -var _ = AfterSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") - By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) +// This test is here for legacy reasons. It spins up a test environment +// with a kubernetes api server and etcd. +// It'll continue to live here for now but all integrations tests should be +// created in the /test/integration directory. +// +// Don't write any tests that use the testEnv or k8sClient in this package +// unless there's a strong reason to. +// +// The tests in the target_controller_test.go and instance_controller_test.go +// are now unit tests that use a mocked k8sClient. All behavior that requires +// a manager and full operator pattern should be tested in the integration tests +var _ = Describe("Legacy testing with envtest", Ordered, func() { + var ( + cancel context.CancelFunc + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + apiClient *MockApiClient + ) + + BeforeAll(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseFlagOptions(&zap.Options{Development: true, TimeEncoder: zapcore.ISO8601TimeEncoder}))) + ctx, cancel = context.WithCancel(context.TODO()) + apiClient = &MockApiClient{} + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "oss", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + //+kubebuilder:scaffold:scheme + err = api.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // The default client created by calling client.New behaves slightly different + // from the client created by the manager. The manager's client has preserves + // the type of the object passed to it. The default client does not. So when you + // make a get call with the default client, the object returned doesn't Have the + // GroupVersionKind set. This would cause some certain assersions to fail as some + // objects are prepared and queried outside the reconciler for test assertions + // for this reaseon, we use the manager's client for all tests. + // + // k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + // needs to disable metrics otherwise all controller suite tests will try to bind to the same port (8080) + MetricsBindAddress: "0", + }) + Expect(err).ToNot(HaveOccurred()) + k8sClient = k8sManager.GetClient() + Expect(k8sClient).NotTo(BeNil()) + + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(BuildDefaultTarget(), ""), nil) + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + + err = (&controllers.TargetReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + ReconciliationInterval: 2 * time.Second, + PollInterval: 1 * time.Second, + DeleteTimeOut: 6 * time.Second, + ApiClient: apiClient, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() + + }) + + AfterAll(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) + }) + + // This doesn't actually test reconcililiation. It only tests that the resources + // can be created in the cluster. The reconciler tests are in the target_controller_test.go and + // instance_controller_test.go files + It("should be able to create valid resources", func() { + By("creating a valid target") + Expect(k8sClient.Create(ctx, BuildDefaultTarget())).To(Succeed()) + }) }) diff --git a/k8s/controllers/fabric/target_controller.go b/k8s/controllers/fabric/target_controller.go index a22a9286d..4f50410fe 100644 --- a/k8s/controllers/fabric/target_controller.go +++ b/k8s/controllers/fabric/target_controller.go @@ -9,35 +9,56 @@ package fabric import ( "context" "fmt" - "strconv" "time" - apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" - symphonyv1 "gopls-workspace/apis/fabric/v1" "gopls-workspace/constants" + "gopls-workspace/controllers/metrics" + "gopls-workspace/predicates" + "gopls-workspace/reconcilers" "gopls-workspace/utils" - provisioningstates "gopls-workspace/utils/models" - "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" - api_utils "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + + apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" ) // TargetReconciler reconciles a Target object type TargetReconciler struct { client.Client Scheme *runtime.Scheme + + // ApiClient is the client for Symphony API + ApiClient utils.ApiClient + + // ReconciliationInterval defines the reconciliation interval + ReconciliationInterval time.Duration + + // DeleteTimeOut defines the timeout for delete operations + DeleteTimeOut time.Duration + + // PollInterval defines the poll interval + PollInterval time.Duration + + // Controller Metrics + m *metrics.Metrics + + dr reconcilers.Reconciler + + // DeleteSyncDelay defines the delay of waiting for status sync back in delete operations + DeleteSyncDelay time.Duration } +const ( + targetFinalizerName = "target.fabric." + constants.FinalizerPostfix + targetOperationStartTimeKey = "target.fabric." + constants.OperationStartTimeKeyPostfix +) + //+kubebuilder:rbac:groups=fabric.symphony,resources=targets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=fabric.symphony,resources=targets/status,verbs=get;update;patch //+kubebuilder:rbac:groups=fabric.symphony,resources=targets/finalizers,verbs=update @@ -52,11 +73,12 @@ type TargetReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - myFinalizerName := "target.fabric.symphony/finalizer" - log := ctrllog.FromContext(ctx) log.Info("Reconcile Target " + req.Name + " in namespace " + req.Namespace) + // Initialize reconcileTime for latency metrics + reconcileTime := time.Now() + // Get target target := &symphonyv1.Target{} @@ -65,239 +87,118 @@ func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, client.IgnoreNotFound(err) } - if target.Status.Properties == nil { - target.Status.Properties = make(map[string]string) - } + reconciliationType := metrics.CreateOperationType + resultType := metrics.ReconcileSuccessResult + reconcileResult := ctrl.Result{} + deploymentOperationType := metrics.DeploymentQueued + var err error if target.ObjectMeta.DeletionTimestamp.IsZero() { // update - if !controllerutil.ContainsFinalizer(target, myFinalizerName) { - controllerutil.AddFinalizer(target, myFinalizerName) - if err := r.Update(ctx, target); err != nil { - return ctrl.Result{}, err - } + reconciliationType = metrics.UpdateOperationType + deploymentOperationType, reconcileResult, err = r.dr.AttemptUpdate(ctx, target, log, targetOperationStartTimeKey) + if err != nil { + resultType = metrics.ReconcileFailedResult } - - summary, err := api_utils.GetSummary(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", fmt.Sprintf("target-runtime-%s", target.ObjectMeta.Name), target.ObjectMeta.Namespace) - if (err != nil && !v1alpha2.IsNotFound(err)) || (err == nil && !summary.IsDeploymentFinished()) { - if err == nil && !summary.IsDeploymentFinished() { - // mock error if deployment is not finished then cause requeue - err = fmt.Errorf("get summary but deployment is not finished yet") - } - uErr := r.updateTargetStatusToReconciling(target, err) - if uErr != nil { - log.Error(uErr, "failed to update target status to reconciling") - return ctrl.Result{}, uErr - } - return ctrl.Result{}, err - } - - generationMatch := true - if v, err := strconv.ParseInt(summary.Generation, 10, 64); err == nil { - generationMatch = v == target.GetGeneration() - } - - if generationMatch && time.Since(summary.Time) <= time.Duration(60)*time.Second { //TODO: this is 60 second interval. Make if configurable? - err = r.updateTargetStatus(target, summary.Summary) - if err != nil { - log.Error(err, "failed to update target status") - return ctrl.Result{}, err - } - return ctrl.Result{RequeueAfter: 60 * time.Second}, nil - } else { - // Queue a job every 60s or when the generation is changed - err = api_utils.QueueJob(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", target.ObjectMeta.Name, target.ObjectMeta.Namespace, false, true) - if err != nil { - uErr := r.updateTargetStatusToReconciling(target, err) - if uErr != nil { - log.Error(uErr, "failed to update target status to reconciling") - return ctrl.Result{}, uErr - } - return ctrl.Result{}, err - } - - // Update status to Reconciling if there is a change on generation - // If users uninstall a component manually without modifying manifest - // files, jobs queued every 60s will catch the descrepdency and - // re-deploy the uninstalled component. As users' behavior doesn't - // trigger generation change, this behavior won't change the status - // to reconciling. - if !generationMatch { - err = r.updateTargetStatusToReconciling(target, nil) - if err != nil { - log.Error(err, "failed to update target status to reconciling") - return ctrl.Result{}, err - } - } - - return ctrl.Result{RequeueAfter: 60 * time.Second}, nil - } - } else { // remove - if controllerutil.ContainsFinalizer(target, myFinalizerName) { - err := api_utils.QueueJob(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", target.ObjectMeta.Name, target.ObjectMeta.Namespace, true, true) - - if err != nil { - uErr := r.updateTargetStatusToReconciling(target, err) - if uErr != nil { - log.Error(uErr, "failed to update target status to reconciling") - return ctrl.Result{}, uErr - } - return ctrl.Result{}, err - } - timeout := time.After(5 * time.Minute) - ticker := time.Tick(10 * time.Second) //TODO: configurable? adjust based on provider SLA? - loop: - for { - select { - case <-timeout: - // Timeout exceeded, assume deletion failed and proceed with finalization - break loop - case <-ticker: - summary, err := api_utils.GetSummary(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", fmt.Sprintf("target-runtime-%s", target.ObjectMeta.Name), target.ObjectMeta.Namespace) - if err == nil && summary.Summary.IsRemoval && summary.IsDeploymentFinished() && summary.Summary.AllAssignedDeployed { - break loop - } - if err != nil && !v1alpha2.IsNotFound(err) { - log.Error(err, "failed to get target summary") - break loop - } - } - } - // NOTE: we assume the message backend provides at-least-once delivery so that the removal event will be eventually handled. - // Until the corresponding provider can successfully carry out the removal job, the job event will remain available for the - // provider to pick up. - controllerutil.RemoveFinalizer(target, myFinalizerName) - if err := r.Update(ctx, target); err != nil { - log.Error(err, "failed to remove finalizer") - return ctrl.Result{}, err - } + deploymentOperationType, reconcileResult, err = r.dr.AttemptRemove(ctx, target, log, targetOperationStartTimeKey) + if err != nil { + resultType = metrics.ReconcileFailedResult } } - return ctrl.Result{}, nil + r.m.ControllerReconcileLatency( + reconcileTime, + reconciliationType, + resultType, + metrics.InstanceResourceType, + deploymentOperationType, + ) + + return reconcileResult, err } -// updateTargetStatusToReconciling updates Target object to Reconciling (non-terminal) state -func (r *TargetReconciler) updateTargetStatusToReconciling(target *symphonyv1.Target, err error) error { - if target.Status.Properties == nil { - target.Status.Properties = make(map[string]string) - } - target.Status.Properties["status"] = provisioningstates.Reconciling - target.Status.Properties["deployed"] = "pending" - target.Status.Properties["targets"] = "pending" - target.Status.Properties["status-details"] = "" +// SetupWithManager sets up the controller with the Manager. +func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error { + metrics, err := metrics.New() if err != nil { - target.Status.Properties["status-details"] = fmt.Sprintf("Reconciling due to %s", err.Error()) + return err } - r.updateProvisioningStatusToReconciling(target, err) - target.Status.LastModified = metav1.Now() - return r.Status().Update(context.Background(), target) -} -func (r *TargetReconciler) updateTargetStatus(target *symphonyv1.Target, summary model.SummarySpec) error { - if target.Status.Properties == nil { - target.Status.Properties = make(map[string]string) - } - targetCount := strconv.Itoa(summary.TargetCount) - successCount := strconv.Itoa(summary.SuccessCount) - status := provisioningstates.Succeeded - if !summary.AllAssignedDeployed { - status = provisioningstates.Failed - } - target.Status.Properties["status"] = status - target.Status.Properties["deployed"] = successCount - target.Status.Properties["targets"] = targetCount - target.Status.Properties["status-details"] = summary.SummaryMessage - // If a component is ever deployed, it will always show in Status.Properties - // If a component is not deleted, it will first be reset to Untouched and - // then changed to corresponding status later - for k, v := range target.Status.Properties { - if utils.IsComponentKey(k) && v != v1alpha2.Deleted.String() { - target.Status.Properties[k] = v1alpha2.Untouched.String() - } - } + r.m = metrics + genChangePredicate := predicate.GenerationChangedPredicate{} + operationIdPredicate := predicates.OperationIdPredicate{} - // Change to corresponding status - for k, v := range summary.TargetResults { - target.Status.Properties["targets."+k] = fmt.Sprintf("%s - %s", v.Status, v.Message) - for kc, c := range v.ComponentResults { - if c.Message == "" { - target.Status.Properties["targets."+k+"."+kc] = c.Status.String() - } else { - target.Status.Properties["targets."+k+"."+kc] = fmt.Sprintf("%s - %s", c.Status, c.Message) - } - } + r.dr, err = r.buildDeploymentReconciler() + if err != nil { + return err } - - r.updateProvisioningStatus(target, status, summary) - target.Status.LastModified = metav1.Now() - return r.Status().Update(context.Background(), target) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error { - genChangePredicate := predicate.GenerationChangedPredicate{} - annotationPredicate := predicate.AnnotationChangedPredicate{} return ctrl.NewControllerManagedBy(mgr). - WithEventFilter(predicate.Or(genChangePredicate, annotationPredicate)). + WithEventFilter(predicate.Or(genChangePredicate, operationIdPredicate)). For(&symphonyv1.Target{}). Complete(r) } -func (r *TargetReconciler) ensureOperationState(target *symphonyv1.Target, provisioningState string) { - target.Status.ProvisioningStatus.Status = provisioningState - target.Status.ProvisioningStatus.OperationID = target.ObjectMeta.Annotations[constants.AzureOperationKey] -} +func (r *TargetReconciler) populateProvisioningError(summaryResult *model.SummaryResult, err error, errorObj *apimodel.ErrorType) { + errorObj.Code = "Symphony Orchestrator: [500]" + if summaryResult != nil { + summary := summaryResult.Summary -func (r *TargetReconciler) updateProvisioningStatus(target *symphonyv1.Target, provisioningStatus string, summary model.SummarySpec) { - r.ensureOperationState(target, provisioningStatus) - // Start with a clean Error object and update all the fields - target.Status.ProvisioningStatus.Error = apimodel.ErrorType{} - // Output field is updated if status is Succeeded - target.Status.ProvisioningStatus.Output = make(map[string]string) - - if provisioningStatus == provisioningstates.Failed { - errorObj := &target.Status.ProvisioningStatus.Error + // Additional message besides target level status(mostly, error message + // but with lower priority than target level error message) + if summary.IsRemoval { + errorObj.Message = fmt.Sprintf("Uninstall failed. %s", summary.SummaryMessage) + } else { + errorObj.Message = fmt.Sprintf("Deployment failed. %s", summary.SummaryMessage) + } // Fill error details into target - errorObj.Code = "Symphony: [500]" - errorObj.Message = "Deployment failed." - errorObj.Target = "Symphony" - errorObj.Details = make([]apimodel.TargetError, 0) + // We assume there is one and only one target in summary spec. As opposed + // to instance CR error object, target CR error object is one layer less. [TODO: We probably shouldn't do this] for k, v := range summary.TargetResults { - targetObject := apimodel.TargetError{ - Code: v.Status, - Message: v.Message, - Target: k, - Details: make([]apimodel.ComponentError, 0), - } + // fill errorObj with target level status + errorObj.Code = v.Status + errorObj.Message = v.Message + errorObj.Target = k + errorObj.Details = make([]apimodel.TargetError, 0) + // fill errorObj.Details with component level status for ck, cv := range v.ComponentResults { - targetObject.Details = append(targetObject.Details, apimodel.ComponentError{ + errorObj.Details = append(errorObj.Details, apimodel.TargetError{ Code: cv.Status.String(), Message: cv.Message, Target: ck, }) } - errorObj.Details = append(errorObj.Details, targetObject) - } - } else if provisioningStatus == provisioningstates.Succeeded { - outputMap := target.Status.ProvisioningStatus.Output - // Fill component details into output field - for k, v := range summary.TargetResults { - for ck, cv := range v.ComponentResults { - outputMap[fmt.Sprintf("%s.%s", k, ck)] = cv.Status.String() - } } } + if err != nil { + errorObj.Message = fmt.Sprintf("%s, %s", err.Error(), errorObj.Message) + } } -// updateProvisioningStatusToReconciling updates ProvisioningStatus to Reconciling (non-terminal) state -func (r *TargetReconciler) updateProvisioningStatusToReconciling(target *symphonyv1.Target, err error) { - provisioningStatus := provisioningstates.Reconciling - if err != nil { - provisioningStatus = fmt.Sprintf("%s: due to %s", provisioningstates.Reconciling, err.Error()) +func (r *TargetReconciler) deploymentBuilder(ctx context.Context, object reconcilers.Reconcilable) (*model.DeploymentSpec, error) { + if target, ok := object.(*symphonyv1.Target); ok { + deployment, err := utils.CreateSymphonyDeploymentFromTarget(*target, object.GetNamespace()) + if err != nil { + return nil, err + } + return &deployment, nil } - r.ensureOperationState(target, provisioningStatus) - // Start with a clean Error object and update all the fields - target.Status.ProvisioningStatus.Error = apimodel.ErrorType{} + return nil, fmt.Errorf("not able to convert object to target") +} + +func (r *TargetReconciler) buildDeploymentReconciler() (reconcilers.Reconciler, error) { + return reconcilers.NewDeploymentReconciler( + reconcilers.WithApiClient(r.ApiClient), + reconcilers.WithDeleteTimeOut(r.DeleteTimeOut), + reconcilers.WithPollInterval(r.PollInterval), + reconcilers.WithClient(r.Client), + reconcilers.WithReconciliationInterval(r.ReconciliationInterval), + reconcilers.WithFinalizerName(targetFinalizerName), + reconcilers.WithDeploymentErrorBuilder(r.populateProvisioningError), + reconcilers.WithDeploymentBuilder(r.deploymentBuilder), + reconcilers.WithDeleteSyncDelay(r.DeleteSyncDelay), + reconcilers.WithDeploymentKeyResolver(func(target reconcilers.Reconcilable) string { + return fmt.Sprintf("target-runtime-%s", target.GetName()) + }), + ) } diff --git a/k8s/controllers/fabric/target_controller_test.go b/k8s/controllers/fabric/target_controller_test.go new file mode 100644 index 000000000..dfc82add7 --- /dev/null +++ b/k8s/controllers/fabric/target_controller_test.go @@ -0,0 +1,240 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package fabric + +import ( + "context" + "errors" + + . "gopls-workspace/testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + symphonyv1 "gopls-workspace/apis/fabric/v1" + + apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + + "gopls-workspace/utils" + + "github.com/stretchr/testify/mock" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + kerrors "k8s.io/apimachinery/pkg/api/errors" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +var _ = Describe("Target controller", Ordered, func() { + var apiClient *MockApiClient + var kubeClient client.Client + var controller *TargetReconciler + var target *symphonyv1.Target + var reconcileError error + var reconcileResult ctrl.Result + + BeforeEach(func() { + By("setting up the controller") + + // We'll setup the controller exactly how it would have been setup if it was done by the manager + // This means we'll need to mock out the api client and kube client + var err error + apiClient = &MockApiClient{} + kubeClient = CreateFakeKubeClientForFabricGroup( + BuildDefaultTarget(), + ) + controller = &TargetReconciler{ + Client: kubeClient, + Scheme: kubeClient.Scheme(), + ReconciliationInterval: TestReconcileInterval, + PollInterval: TestPollInterval, + DeleteTimeOut: TestReconcileTimout, + ApiClient: apiClient, + } + + controller.dr, err = controller.buildDeploymentReconciler() + Expect(err).ToNot(HaveOccurred()) + }) + + Describe("Reconcile", func() { + BeforeEach(func(ctx context.Context) { + By("fetching resource") + target = &symphonyv1.Target{} + Expect(kubeClient.Get(ctx, DefaultTargetNamepsacedName, target)).To(Succeed()) + }) + + JustBeforeEach(func(ctx context.Context) { + By("simulating a reconcile event") + reconcileResult, reconcileError = controller.Reconcile(ctx, ctrl.Request{NamespacedName: DefaultTargetNamepsacedName}) + }) + + When("the target is created", func() { + JustBeforeEach(func(ctx context.Context) { + By("fetching the target") + Expect(kubeClient.Get(ctx, DefaultTargetNamepsacedName, target)).To(Succeed()) + }) + + Context("and the deployment completed successfully", func() { + BeforeEach(func() { + By("mocking the get summary call to return a successful deployment") + hash := utils.HashObjects(utils.DeploymentResources{TargetCandidates: []symphonyv1.Target{*target}}) + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(target, hash), nil) + }) + + It("should not return an error", func() { + Expect(reconcileError).ToNot(HaveOccurred()) + }) + + It("should requeue after the reconciliation interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1s").Of(controller.ReconciliationInterval)) + }) + }) + + Context("and the deployment failed due to some error", func() { + BeforeEach(func() { + By("mocking the get summary call to return a not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking a failed deployment to the api") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("some error")) + }) + + It("should queue another reconciliation as soon as possible", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + + It("should have a status of reconciling", func() { + Expect(target.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + + Context("and the deployment completed with errors", func() { + BeforeEach(func() { + By("mocking the get summary call to return a successful deployment") + hash := utils.HashObjects(utils.DeploymentResources{TargetCandidates: []symphonyv1.Target{*target}}) + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockFailureSummaryResult(target, hash), nil) + }) + + It("should not return an error", func() { + Expect(reconcileError).ToNot(HaveOccurred()) + }) + + It("should requeue after the reconciliation interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1s").Of(controller.ReconciliationInterval)) + }) + + It("should have a status of failed", func() { + Expect(target.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + + It("should have custom summary of errors", func() { + Expect(target.Status.ProvisioningStatus.Error.Code).To(Equal("ErrorCode")) + Expect(target.Status.ProvisioningStatus.Error.Details).To(ContainElement(apimodel.TargetError{ + Code: "Update Failed", + Message: "failed", + Target: "comp1", + })) + }) + }) + }) + + When("the target is not found", func() { + BeforeEach(func(ctx context.Context) { + By("deleting the target") + Expect(kubeClient.Delete(ctx, target)).To(Succeed()) + }) + + It("should not return an error", func() { + Expect(reconcileError).ToNot(HaveOccurred()) + }) + }) + + When("the target is marked for deletion", func() { + BeforeEach(func(ctx context.Context) { + By("adding a finalizer to the target") + target.SetFinalizers([]string{targetFinalizerName}) + + By("updating the target") + Expect(kubeClient.Update(ctx, target)).To(Succeed()) + Expect(kubeClient.Get(ctx, DefaultTargetNamepsacedName, target)).To(Succeed()) + Expect(target.GetFinalizers()).To(ContainElement(targetFinalizerName)) + }) + + BeforeEach(func(ctx context.Context) { + By("deleting the target") + Expect(kubeClient.Delete(ctx, target)).To(Succeed()) + }) + + Context("and the deletion deployment is successful", func() { + BeforeEach(func(ctx context.Context) { + By("simulating a completed delete deployment from the api") + hash := utils.HashObjects(utils.DeploymentResources{TargetCandidates: []symphonyv1.Target{*target}}) + summary := MockSucessSummaryResult(target, hash) + summary.Summary.IsRemoval = true + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should no longer exist in the kubernetes api", func(ctx context.Context) { + By("fetching the updated target") + err := kubeClient.Get(ctx, DefaultTargetNamepsacedName, target) + Expect(kerrors.IsNotFound(err)).To(BeTrue()) + }) + + It("should not return an error", func() { + Expect(reconcileError).ToNot(HaveOccurred()) + }) + }) + + Context("and the deletion deployment is still in progress", func() { + BeforeEach(func(ctx context.Context) { + By("simulating a pending delete deployment from the api") + hash := utils.HashObjects(utils.DeploymentResources{TargetCandidates: []symphonyv1.Target{*target}}) + summary := MockInProgressDeleteSummaryResult(target, hash) + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + JustBeforeEach(func(ctx context.Context) { + By("fetching the target") + Expect(kubeClient.Get(ctx, DefaultTargetNamepsacedName, target)).To(Succeed()) + }) + + It("should not return an error", func() { + Expect(reconcileError).ToNot(HaveOccurred()) + }) + + It("should have a status of deleting", func() { + Expect(target.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + + It("should requeue after the poll interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1s").Of(controller.PollInterval)) + }) + }) + + Context("and the deletion deployment failed due to random error", func() { + BeforeEach(func(ctx context.Context) { + By("simulating a failed delete deployment from the api") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("some error")) + }) + + JustBeforeEach(func(ctx context.Context) { + By("fetching the target") + Expect(kubeClient.Get(ctx, DefaultTargetNamepsacedName, target)).To(Succeed()) + }) + + It("should have a status of deleting", func() { + Expect(target.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + + It("should requeue as soon as possible due to error", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + }) + }) + }) +}) diff --git a/k8s/controllers/metrics/attributes.go b/k8s/controllers/metrics/attributes.go new file mode 100644 index 000000000..0416c566b --- /dev/null +++ b/k8s/controllers/metrics/attributes.go @@ -0,0 +1,24 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package metrics + +// Deployment gets common logging attributes for a deployment. +func Deployment( + reconciliationType ReconciliationType, + reconciliationResult ReconciliationResult, + resourceType ResourceType, + operationStatus OperationStatus, + chartVersion string, +) map[string]any { + return map[string]any{ + "reconciliationType": reconciliationType, + "reconciliationResult": reconciliationResult, + "resourceType": resourceType, + "operationStatus": operationStatus, + "chartVersion": chartVersion, + } +} diff --git a/k8s/controllers/metrics/metrics.go b/k8s/controllers/metrics/metrics.go new file mode 100644 index 000000000..1bbd0f3f2 --- /dev/null +++ b/k8s/controllers/metrics/metrics.go @@ -0,0 +1,104 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package metrics + +import ( + "gopls-workspace/constants" + "os" + "time" + + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" +) + +type ( + ReconciliationType string + ReconciliationResult string + ResourceType string + OperationStatus string +) + +const ( + // reconciliation type + CreateOperationType ReconciliationType = "Create" + UpdateOperationType ReconciliationType = "Update" + DeleteOperationType ReconciliationType = "Delete" + // resource type + TargetResourceType ResourceType = "Target" + InstanceResourceType ResourceType = "Instance" + // reconciliation result + ReconcileSuccessResult ReconciliationResult = "Succeeded" + ReconcileFailedResult ReconciliationResult = "Failed" + // operation status + StatusNoOp OperationStatus = "NoOp" + StatusUpdateFailed OperationStatus = "StatusUpdateFailed" + // deployment operation status + DeploymentQueued OperationStatus = "DeploymentQueued" + DeploymentStatusPolled OperationStatus = "DeploymentStatusPolled" + DeploymentSucceeded OperationStatus = "DeploymentSucceeded" + DeploymentFailed OperationStatus = "DeploymentFailed" + DeploymentTimedOut OperationStatus = "DeploymentTimedOut" + GetDeploymentSummaryFailed OperationStatus = "GetDeploymentSummaryFailed" + QueueDeploymentFailed OperationStatus = "QueueDeploymentFailed" +) + +// Metrics is a metrics tracker for a controller operation. +type Metrics struct { + controllerReconcileLatency observability.Histogram +} + +func New() (*Metrics, error) { + observable := observability.New(constants.K8S) + + controllerReconcileLatency, err := observable.Metrics.Histogram( + "symphony_controller_reconcile_latency", + "measure of overall latency for controller operation side", + ) + if err != nil { + return nil, err + } + + return &Metrics{ + controllerReconcileLatency: controllerReconcileLatency, + }, nil +} + +// Close closes all metrics. +func (m *Metrics) Close() { + if m == nil { + return + } +} + +// ControllerReconcileLatency tracks the overall Controller reconcile latency. +func (m *Metrics) ControllerReconcileLatency( + startTime time.Time, + reconcilationType ReconciliationType, + reconcilationResult ReconciliationResult, + resourceType ResourceType, + operationStatus OperationStatus, +) { + if m == nil { + return + } + + chartVersion := os.Getenv("CHART_VERSION") + m.controllerReconcileLatency.Add( + latency(startTime), + Deployment( + reconcilationType, + reconcilationResult, + resourceType, + operationStatus, + chartVersion, + ), + ) +} + +// Latency gets the time since the given start in milliseconds. +func latency(start time.Time) float64 { + return float64(time.Since(start)) / float64(time.Millisecond) +} diff --git a/k8s/controllers/solution/instance_controller.go b/k8s/controllers/solution/instance_controller.go index 63556c91f..e4a010d08 100644 --- a/k8s/controllers/solution/instance_controller.go +++ b/k8s/controllers/solution/instance_controller.go @@ -9,40 +9,62 @@ package solution import ( "context" "fmt" - "strconv" + "strings" "time" - symphonyv1 "gopls-workspace/apis/solution/v1" + fabric_v1 "gopls-workspace/apis/fabric/v1" + solution_v1 "gopls-workspace/apis/solution/v1" + "gopls-workspace/controllers/metrics" + "gopls-workspace/predicates" + "gopls-workspace/reconcilers" "gopls-workspace/constants" "gopls-workspace/utils" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" - apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" - provisioningstates "github.com/eclipse-symphony/symphony/k8s/utils/models" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/source" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - api_utils "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" ) // InstanceReconciler reconciles a Instance object type InstanceReconciler struct { client.Client Scheme *runtime.Scheme + + // ApiClient is the client for Symphony API + ApiClient utils.ApiClient + + // ReconciliationInterval defines the reconciliation interval + ReconciliationInterval time.Duration + + // DeleteTimeOut defines the timeout for delete operations + DeleteTimeOut time.Duration + + // PollInterval defines the poll interval + PollInterval time.Duration + + // Controller metrics + m *metrics.Metrics + + dr reconcilers.Reconciler + + // DeleteSyncDelay defines the delay of waiting for status sync back in delete operations + DeleteSyncDelay time.Duration } +const ( + instanceFinalizerName = "instance.solution." + constants.FinalizerPostfix + instanceOperationStartTimeKey = "instance.solution." + constants.OperationStartTimeKeyPostfix +) + //+kubebuilder:rbac:groups=solution.symphony,resources=instances,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=solution.symphony,resources=instances/status,verbs=get;update;patch //+kubebuilder:rbac:groups=solution.symphony,resources=instances/finalizers,verbs=update @@ -57,267 +79,186 @@ type InstanceReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile func (r *InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - myFinalizerName := "instance.solution.symphony/finalizer" - log := ctrllog.FromContext(ctx) log.Info("Reconcile Instance " + req.Name + " in namespace " + req.Namespace) + // Initialize reconcileTime for latency metrics + reconcileTime := time.Now() + // Get instance - instance := &symphonyv1.Instance{} + instance := &solution_v1.Instance{} if err := r.Client.Get(ctx, req.NamespacedName, instance); err != nil { log.Error(err, "unable to fetch Instance object") return ctrl.Result{}, client.IgnoreNotFound(err) } - if instance.Status.Properties == nil { - instance.Status.Properties = make(map[string]string) - } + reconciliationType := metrics.CreateOperationType + resultType := metrics.ReconcileSuccessResult + reconcileResult := ctrl.Result{} + deploymentOperationType := metrics.DeploymentQueued + var err error if instance.ObjectMeta.DeletionTimestamp.IsZero() { // update - if !controllerutil.ContainsFinalizer(instance, myFinalizerName) { - controllerutil.AddFinalizer(instance, myFinalizerName) - if err := r.Client.Update(ctx, instance); err != nil { - return ctrl.Result{}, err - } - } - - summary, err := api_utils.GetSummary(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", instance.ObjectMeta.Name, instance.ObjectMeta.Namespace) - if (err != nil && !v1alpha2.IsNotFound(err)) || (err == nil && !summary.IsDeploymentFinished()) { - if err == nil && !summary.IsDeploymentFinished() { - // mock error if deployment is not finished then cause requeue - err = fmt.Errorf("get summary but deployment is not finished yet") - } - uErr := r.updateInstanceStatusToReconciling(instance, err) - if uErr != nil { - return ctrl.Result{}, uErr - } - return ctrl.Result{}, err - } - - generationMatch := true - if v, err := strconv.ParseInt(summary.Generation, 10, 64); err == nil { - generationMatch = v == instance.GetGeneration() - } - if generationMatch && time.Since(summary.Time) <= time.Duration(60)*time.Second { //TODO: this is 60 second interval. Make if configurable? - err = r.updateInstanceStatus(instance, summary.Summary) - if err != nil { - return ctrl.Result{}, err - } - return ctrl.Result{RequeueAfter: 60 * time.Second}, nil - } else { - // Queue a job every 60s or when the generation is changed - err = api_utils.QueueJob(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", instance.ObjectMeta.Name, instance.ObjectMeta.Namespace, false, false) - if err != nil { - uErr := r.updateInstanceStatusToReconciling(instance, err) - if uErr != nil { - return ctrl.Result{}, uErr - } - return ctrl.Result{}, err - } - - // Update status to Reconciling if there is a change on generation - // If users uninstall a component manually without modifying manifest - // files, jobs queued every 60s will catch the descrepdency and - // re-deploy the uninstalled component. As users' behavior doesn't - // trigger generation change, this behavior won't change the status - // to reconciling. - if !generationMatch { - err = r.updateInstanceStatusToReconciling(instance, nil) - if err != nil { - return ctrl.Result{}, err - } - } - - return ctrl.Result{RequeueAfter: 60 * time.Second}, nil + reconciliationType = metrics.UpdateOperationType + deploymentOperationType, reconcileResult, err = r.dr.AttemptUpdate(ctx, instance, log, instanceOperationStartTimeKey) + if err != nil { + resultType = metrics.ReconcileFailedResult } - } else { // delete - if controllerutil.ContainsFinalizer(instance, myFinalizerName) { - err := api_utils.QueueJob(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", instance.ObjectMeta.Name, instance.ObjectMeta.Namespace, true, false) - - if err != nil { - uErr := r.updateInstanceStatusToReconciling(instance, err) - if uErr != nil { - return ctrl.Result{}, uErr - } - return ctrl.Result{}, err - } - timeout := time.After(5 * time.Minute) - ticker := time.Tick(10 * time.Second) //TODO: configurable? adjust based on provider SLA? - loop: - for { - select { - case <-timeout: - // Timeout exceeded, assume deletion failed and proceed with finalization - break loop - case <-ticker: - summary, err := api_utils.GetSummary(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", instance.ObjectMeta.Name, instance.ObjectMeta.Namespace) - if err == nil && summary.Summary.IsRemoval && summary.IsDeploymentFinished() && summary.Summary.SuccessCount == summary.Summary.TargetCount { - break loop - } - if err != nil && v1alpha2.IsNotFound(err) { - break loop - } - } - } - // NOTE: we assume the message backend provides at-least-once delivery so that the removal event will be eventually handled. - // Until the corresponding provider can successfully carry out the removal job, the job event will remain available for the - // provider to pick up. - controllerutil.RemoveFinalizer(instance, myFinalizerName) - if err := r.Client.Update(ctx, instance); err != nil { - return ctrl.Result{}, err - } + } else { // remove + deploymentOperationType, reconcileResult, err = r.dr.AttemptRemove(ctx, instance, log, instanceOperationStartTimeKey) + if err != nil { + resultType = metrics.ReconcileFailedResult } } - return ctrl.Result{}, nil -} -func (r *InstanceReconciler) ensureOperationState(instance *symphonyv1.Instance, provisioningState string) { - instance.Status.ProvisioningStatus.Status = provisioningState - instance.Status.ProvisioningStatus.OperationID = instance.ObjectMeta.Annotations[constants.AzureOperationKey] + r.m.ControllerReconcileLatency( + reconcileTime, + reconciliationType, + resultType, + metrics.InstanceResourceType, + deploymentOperationType, + ) + + return reconcileResult, err } -// updateInstanceStatusToReconciling updates Instance object to Reconciling (non-terminal) state -func (r *InstanceReconciler) updateInstanceStatusToReconciling(instance *symphonyv1.Instance, err error) error { - if instance.Status.Properties == nil { - instance.Status.Properties = make(map[string]string) +func (r *InstanceReconciler) deploymentBuilder(ctx context.Context, object reconcilers.Reconcilable) (*model.DeploymentSpec, error) { + log := ctrllog.FromContext(ctx) + log.Info("Building deployment") + var deployment model.DeploymentSpec + instance, ok := object.(*solution_v1.Instance) + if !ok { + return nil, v1alpha2.NewCOAError(nil, "not able to convert object to instance", v1alpha2.ObjectInstanceCoversionFailed) } - instance.Status.Properties["status"] = provisioningstates.Reconciling - instance.Status.Properties["deployed"] = "pending" - instance.Status.Properties["targets"] = "pending" - instance.Status.Properties["status-details"] = "" - if err != nil { - instance.Status.Properties["status-details"] = fmt.Sprintf("Reconciling due to %s", err.Error()) + + deploymentResources := &utils.DeploymentResources{ + Instance: *instance, + Solution: solution_v1.Solution{}, + TargetList: fabric_v1.TargetList{}, + TargetCandidates: []fabric_v1.Target{}, } - r.updateProvisioningStatusToReconciling(instance, err) - instance.Status.LastModified = metav1.Now() - return r.Client.Status().Update(context.Background(), instance) -} -func (r *InstanceReconciler) updateInstanceStatus(instance *symphonyv1.Instance, summary model.SummarySpec) error { - if instance.Status.Properties == nil { - instance.Status.Properties = make(map[string]string) + + if err := r.Get(ctx, types.NamespacedName{Name: instance.Spec.Solution, Namespace: instance.Namespace}, &deploymentResources.Solution); err != nil { + log.Error(v1alpha2.NewCOAError(err, "failed to get solution", v1alpha2.SolutionGetFailed), "proceed with no solution found") } - targetCount := strconv.Itoa(summary.TargetCount) - successCount := strconv.Itoa(summary.SuccessCount) - status := provisioningstates.Succeeded - if !summary.AllAssignedDeployed { - status = provisioningstates.Failed + // Get targets + if err := r.List(ctx, &deploymentResources.TargetList, client.InNamespace(instance.Namespace)); err != nil { + log.Error(v1alpha2.NewCOAError(err, "failed to list targets", v1alpha2.TargetListGetFailed), "proceed with no targets found") } - instance.Status.Properties["status"] = status - instance.Status.Properties["deployed"] = successCount - instance.Status.Properties["targets"] = targetCount - instance.Status.Properties["status-details"] = summary.SummaryMessage - - // If a component is ever deployed, it will always show in Status.Properties - // If a component is not deleted, it will first be reset to Untouched and - // then changed to corresponding status later - for k, v := range instance.Status.Properties { - if utils.IsComponentKey(k) && v != v1alpha2.Deleted.String() { - instance.Status.Properties[k] = v1alpha2.Untouched.String() - } + // Get target candidates + deploymentResources.TargetCandidates = utils.MatchTargets(*instance, deploymentResources.TargetList) + if len(deploymentResources.TargetCandidates) == 0 { + log.Error(v1alpha2.NewCOAError(nil, "no target candidates found", v1alpha2.TargetCandidatesNotFound), "proceed with no target candidates found") } - // Change to corresponding status - for k, v := range summary.TargetResults { - instance.Status.Properties["targets."+k] = fmt.Sprintf("%s - %s", v.Status, v.Message) - for ck, cv := range v.ComponentResults { - if cv.Message == "" { - instance.Status.Properties["targets."+k+"."+ck] = cv.Status.String() - } else { - instance.Status.Properties["targets."+k+"."+ck] = fmt.Sprintf("%s - %s", cv.Status, cv.Message) - } - } + deployment, err := utils.CreateSymphonyDeployment(ctx, *instance, deploymentResources.Solution, deploymentResources.TargetCandidates, object.GetNamespace()) + if err != nil { + return nil, err } + return &deployment, nil +} - r.updateProvisioningStatus(instance, status, summary) - instance.Status.LastModified = metav1.Now() - return r.Client.Status().Update(context.Background(), instance) +func (r *InstanceReconciler) buildDeploymentReconciler() (reconcilers.Reconciler, error) { + return reconcilers.NewDeploymentReconciler( + reconcilers.WithApiClient(r.ApiClient), + reconcilers.WithDeleteTimeOut(r.DeleteTimeOut), + reconcilers.WithPollInterval(r.PollInterval), + reconcilers.WithClient(r.Client), + reconcilers.WithReconciliationInterval(r.ReconciliationInterval), + reconcilers.WithDeleteSyncDelay(r.DeleteSyncDelay), + reconcilers.WithFinalizerName(instanceFinalizerName), + reconcilers.WithDeploymentBuilder(r.deploymentBuilder), + ) } -func (r *InstanceReconciler) updateProvisioningStatus(instance *symphonyv1.Instance, provisioningStatus string, summary model.SummarySpec) { - r.ensureOperationState(instance, provisioningStatus) - // Start with a clean Error object and update all the fields - instance.Status.ProvisioningStatus.Error = apimodel.ErrorType{} - // Output field is updated if status is Succeeded - instance.Status.ProvisioningStatus.Output = make(map[string]string) - - if provisioningStatus == provisioningstates.Failed { - errorObj := &instance.Status.ProvisioningStatus.Error - - // Fill error details into error object - errorObj.Code = "Symphony: [500]" - errorObj.Message = "Deployment failed." - errorObj.Target = "Symphony" - errorObj.Details = make([]apimodel.TargetError, 0) - for k, v := range summary.TargetResults { - targetObject := apimodel.TargetError{ - Code: v.Status, - Message: v.Message, - Target: k, - Details: make([]apimodel.ComponentError, 0), - } - for ck, cv := range v.ComponentResults { - targetObject.Details = append(targetObject.Details, apimodel.ComponentError{ - Code: cv.Status.String(), - Message: cv.Message, - Target: ck, - }) - } - errorObj.Details = append(errorObj.Details, targetObject) - } - } else if provisioningStatus == provisioningstates.Succeeded { - outputMap := instance.Status.ProvisioningStatus.Output - // Fill component details into output field - for k, v := range summary.TargetResults { - for ck, cv := range v.ComponentResults { - outputMap[fmt.Sprintf("%s.%s", k, ck)] = cv.Status.String() - } - } +// SetupWithManager sets up the controller with the Manager. +func (r *InstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { + var err error + if r.m, err = metrics.New(); err != nil { + return err } -} -// updateProvisioningStatusToReconciling updates ProvisioningStatus to Reconciling (non-terminal) state -func (r *InstanceReconciler) updateProvisioningStatusToReconciling(instance *symphonyv1.Instance, err error) { - provisioningStatus := provisioningstates.Reconciling - if err != nil { - provisioningStatus = fmt.Sprintf("%s: due to %s", provisioningstates.Reconciling, err.Error()) + if r.dr, err = r.buildDeploymentReconciler(); err != nil { + return err } - r.ensureOperationState(instance, provisioningStatus) - // Start with a clean Error object and update all the fields - instance.Status.ProvisioningStatus.Error = apimodel.ErrorType{} -} -// SetupWithManager sets up the controller with the Manager. -func (r *InstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { generationChange := predicate.GenerationChangedPredicate{} - annotationChange := predicate.AnnotationChangedPredicate{} + operationIdPredicate := predicates.OperationIdPredicate{} return ctrl.NewControllerManagedBy(mgr). - For(&symphonyv1.Instance{}). - WithEventFilter(predicate.Or(generationChange, annotationChange)). - Watches(&source.Kind{Type: &symphonyv1.Solution{}}, handler.EnqueueRequestsFromMapFunc( - func(obj client.Object) []ctrl.Request { - ret := make([]ctrl.Request, 0) - solObj := obj.(*symphonyv1.Solution) - var instances symphonyv1.InstanceList - options := []client.ListOption{ - client.InNamespace(solObj.Namespace), - client.MatchingFields{"spec.solution": solObj.Name}, - } - error := mgr.GetClient().List(context.Background(), &instances, options...) - if error != nil { - log.Log.Error(error, "Failed to list instances") - return ret - } - - for _, instance := range instances.Items { - ret = append(ret, ctrl.Request{ - NamespacedName: types.NamespacedName{ - Name: instance.Name, - Namespace: instance.Namespace, - }, - }) - } - return ret - })). + For(&solution_v1.Instance{}). + WithEventFilter(predicate.Or(generationChange, operationIdPredicate)). + Watches(&source.Kind{Type: &solution_v1.Solution{}}, handler.EnqueueRequestsFromMapFunc( + r.handleSolution)). + Watches(&source.Kind{Type: &fabric_v1.Target{}}, handler.EnqueueRequestsFromMapFunc( + r.handleTarget)). Complete(r) } + +func (r *InstanceReconciler) handleTarget(obj client.Object) []ctrl.Request { + ret := make([]ctrl.Request, 0) + tarObj := obj.(*fabric_v1.Target) + var instances solution_v1.InstanceList + + options := []client.ListOption{client.InNamespace(tarObj.Namespace)} + err := r.List(context.Background(), &instances, options...) + if err != nil { + log.Log.Error(err, "Failed to list instances") + return ret + } + + targetList := fabric_v1.TargetList{} + targetList.Items = append(targetList.Items, *tarObj) + + updatedInstanceNames := make([]string, 0) + for _, instance := range instances.Items { + targetCandidates := utils.MatchTargets(instance, targetList) + if len(targetCandidates) > 0 { + ret = append(ret, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: instance.Name, + Namespace: instance.Namespace, + }, + }) + updatedInstanceNames = append(updatedInstanceNames, instance.Name) + } + } + + if len(ret) > 0 { + log.Log.Info(fmt.Sprintf("Watched target %s under namespace %s is updated, needs to requeue instances related, count: %d, list: %s", tarObj.Name, tarObj.Namespace, len(ret), strings.Join(updatedInstanceNames, ","))) + } + + return ret +} + +func (r *InstanceReconciler) handleSolution(obj client.Object) []ctrl.Request { + ret := make([]ctrl.Request, 0) + solObj := obj.(*solution_v1.Solution) + var instances solution_v1.InstanceList + options := []client.ListOption{ + client.InNamespace(solObj.Namespace), + client.MatchingFields{"spec.solution": solObj.Name}, + } + error := r.List(context.Background(), &instances, options...) + if error != nil { + log.Log.Error(error, "Failed to list instances") + return ret + } + + updatedInstanceNames := make([]string, 0) + for _, instance := range instances.Items { + ret = append(ret, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: instance.Name, + Namespace: instance.Namespace, + }, + }) + updatedInstanceNames = append(updatedInstanceNames, instance.Name) + } + + if len(ret) > 0 { + log.Log.Info(fmt.Sprintf("Watched solution %s under namespace %s is updated, needs to requeue instances related, count: %d, list: %s", solObj.Name, solObj.Namespace, len(ret), strings.Join(updatedInstanceNames, ","))) + } + + return ret +} diff --git a/k8s/controllers/solution/instance_controller_test.go b/k8s/controllers/solution/instance_controller_test.go new file mode 100644 index 000000000..82055690f --- /dev/null +++ b/k8s/controllers/solution/instance_controller_test.go @@ -0,0 +1,287 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package solution + +import ( + "context" + "errors" + fabricv1 "gopls-workspace/apis/fabric/v1" + solutionv1 "gopls-workspace/apis/solution/v1" + . "gopls-workspace/testing" + "gopls-workspace/utils" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + kerrors "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("Instance controller", Ordered, func() { + var apiClient *MockApiClient + var kubeClient client.Client + var controller *InstanceReconciler + var instance *solutionv1.Instance + var target *fabricv1.Target + var solution *solutionv1.Solution + var reconcileError error + var reconcileResult ctrl.Result + + BeforeEach(func() { + By("setting up the controller") + + // We'll setup the controller exactly how it would have been setup if it was done by the manager + // This means we'll need to mock out the api client and kube client + var err error + apiClient = &MockApiClient{} + kubeClient = CreateFakeKubeClientForSolutionAndFabricGroup( + BuildDefaultInstance(), + BuildDefaultTarget(), + BuildDefaultSolution(), + ) + controller = &InstanceReconciler{ + Client: kubeClient, + Scheme: kubeClient.Scheme(), + ReconciliationInterval: TestReconcileInterval, + PollInterval: TestPollInterval, + DeleteTimeOut: TestReconcileTimout, + ApiClient: apiClient, + } + + controller.dr, err = controller.buildDeploymentReconciler() + Expect(err).ToNot(HaveOccurred()) + }) + + BeforeEach(func(ctx context.Context) { + By("fetching resources") + instance = &solutionv1.Instance{} + Expect(kubeClient.Get(ctx, DefaultInstanceNamespacedName, instance)).To(Succeed()) + + target = &fabricv1.Target{} + Expect(kubeClient.Get(ctx, DefaultTargetNamepsacedName, target)).To(Succeed()) + + solution = &solutionv1.Solution{} + Expect(kubeClient.Get(ctx, DefaultSolutionNamespacedName, solution)).To(Succeed()) + }) + + Describe("Reconcile", func() { + JustBeforeEach(func(ctx context.Context) { + By("simulating a reconcile event") + reconcileResult, reconcileError = controller.Reconcile(ctx, ctrl.Request{NamespacedName: DefaultInstanceNamespacedName}) + }) + When("the instance is created", func() { + + JustBeforeEach(func(ctx context.Context) { + By("fetching the instance") + Expect(kubeClient.Get(ctx, DefaultInstanceNamespacedName, instance)).To(Succeed()) + }) + + Context("and all necessary resources are present in the cluster", func() { + Context("and the deployment completed successfully", func() { + BeforeEach(func() { + By("mocking the get summary call to return a successful deployment") + hash := utils.HashObjects(utils.DeploymentResources{Instance: *instance, Solution: *solution, TargetCandidates: []fabricv1.Target{*target}}) + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(instance, hash), nil) + }) + + It("should not return an error", func() { + Expect(reconcileError).ToNot(HaveOccurred()) + }) + + It("should requeue after the reconciliation interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1s").Of(controller.ReconciliationInterval)) + }) + }) + Context("and the deployment failed due to some error", func() { + BeforeEach(func() { + By("mocking the get summary call to return a not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking a failed deployment to the api") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("some error")) + }) + + It("should queue anotther reconciliation as soon as possible", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + It("should have a status of reconciling", func() { + Expect(instance.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + }) + + Context("and the solution is not present in the cluster", func() { + BeforeEach(func(ctx context.Context) { + By("deleting the solution") + Expect(kubeClient.Delete(ctx, solution)).To(Succeed()) + + By("mocking a successful deployment to the api") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + BeforeEach(func() { + By("mocking the get summary call to return a not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + }) + + It("should have a status of Reconciling", func() { + Expect(instance.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + + It("should requeue without error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + }) + + Context("and the target is not present in the cluster", func() { + BeforeEach(func(ctx context.Context) { + By("deleting the target") + Expect(kubeClient.Delete(ctx, target)).To(Succeed()) + }) + + BeforeEach(func() { + By("mocking the get summary call to return a not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking a successful deployment to the api") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + It("should have a status of Reconciling", func() { + Expect(instance.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + + It("should requeue without error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + }) + }) + + When("the instance is not found", func() { + BeforeEach(func(ctx context.Context) { + By("deleting the instance") + Expect(kubeClient.Delete(ctx, instance)).To(Succeed()) + }) + + It("should not return an error", func() { + Expect(reconcileError).ToNot(HaveOccurred()) + }) + }) + + When("the instance is marked for deletion", func() { + BeforeEach(func(ctx context.Context) { + By("adding a finalizer to the instance") + instance.SetFinalizers([]string{instanceFinalizerName}) + + By("updating the instance") + Expect(kubeClient.Update(ctx, instance)).To(Succeed()) + Expect(kubeClient.Get(ctx, DefaultInstanceNamespacedName, instance)).To(Succeed()) + Expect(instance.GetFinalizers()).To(ContainElement(instanceFinalizerName)) + }) + + BeforeEach(func(ctx context.Context) { + By("deleting the instance") + Expect(kubeClient.Delete(ctx, instance)).To(Succeed()) + }) + + Context("and the deletion deployment is successful", func() { + BeforeEach(func(ctx context.Context) { + By("simulating a completed delete deployment from the api") + hash := utils.HashObjects(utils.DeploymentResources{Instance: *instance, Solution: *solution, TargetCandidates: []fabricv1.Target{*target}}) + summary := MockSucessSummaryResult(instance, hash) + summary.Summary.IsRemoval = true + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should no longer exist in the kubernetes api", func(ctx context.Context) { + By("fetching the updated instance") + err := kubeClient.Get(ctx, DefaultInstanceNamespacedName, instance) + Expect(kerrors.IsNotFound(err)).To(BeTrue()) + }) + + It("should not return an error", func() { + Expect(reconcileError).ToNot(HaveOccurred()) + }) + }) + + Context("and the deletion deployment is still in progress", func() { + BeforeEach(func(ctx context.Context) { + By("simulating a pending delete deployment from the api") + hash := utils.HashObjects(utils.DeploymentResources{Instance: *instance, Solution: *solution, TargetCandidates: []fabricv1.Target{*target}}) + summary := MockInProgressDeleteSummaryResult(instance, hash) + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + JustBeforeEach(func(ctx context.Context) { + By("fetching the instance") + Expect(kubeClient.Get(ctx, DefaultInstanceNamespacedName, instance)).To(Succeed()) + }) + + It("should not return an error", func() { + Expect(reconcileError).ToNot(HaveOccurred()) + }) + + It("should have a status of deleting", func() { + Expect(instance.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + + It("should requeue after the poll interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1s").Of(controller.PollInterval)) + }) + }) + + Context("and the deletion deployment failed due to random error", func() { + BeforeEach(func(ctx context.Context) { + By("simulating a failed delete deployment from the api") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("some error")) + }) + + JustBeforeEach(func(ctx context.Context) { + By("fetching the instance") + Expect(kubeClient.Get(ctx, DefaultInstanceNamespacedName, instance)).To(Succeed()) + }) + + It("should have a status of deleting", func() { + Expect(instance.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + + It("should requeue as soon as possible due to error", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + }) + }) + }) + + Describe("Solution Events", func() { + When("the solution referenced by the instance is changed", func() { + var requests []ctrl.Request + BeforeEach(func(ctx context.Context) { + By("simulating a call to the handleSolution function") + requests = controller.handleSolution(solution) + }) + + It("should return a request for the instance", func() { + Expect(requests).To(ContainElement(ctrl.Request{NamespacedName: DefaultInstanceNamespacedName})) + }) + }) + }) + + Describe("Target Events", func() { + When("the target referenced by the instance is changed", func() { + var requests []ctrl.Request + BeforeEach(func(ctx context.Context) { + By("simulating a call to the handleTarget function") + requests = controller.handleTarget(target) + }) + + It("should return a request for the instance", func() { + Expect(requests).To(ContainElement(ctrl.Request{NamespacedName: DefaultInstanceNamespacedName})) + }) + }) + }) +}) diff --git a/k8s/controllers/solution/suite_test.go b/k8s/controllers/solution/suite_test.go index 04a356d48..1a4933b6f 100644 --- a/k8s/controllers/solution/suite_test.go +++ b/k8s/controllers/solution/suite_test.go @@ -4,12 +4,14 @@ * SPDX-License-Identifier: MIT */ -package solution +package solution_test import ( + "context" "encoding/json" "path/filepath" "testing" + "time" . "gopls-workspace/testing" @@ -17,6 +19,8 @@ import ( . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/zap/zapcore" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -25,26 +29,23 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/yaml" - solutionv1 "gopls-workspace/apis/solution/v1" + api "gopls-workspace/apis/solution/v1" + controllers "gopls-workspace/controllers/solution" + + ctrl "sigs.k8s.io/controller-runtime" //+kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment - func TestAPIs(t *testing.T) { - t.Skip("Skipping tests for now as they are no longer relevant") RegisterFailHandler(Fail) RunGinkgoSpecs(t, "Controller Suite") } func TestUnmarshalSolution(t *testing.T) { - t.Skip("Skipping tests for now as they are no longer relevant") solutionYaml := `apiVersion: solution.symphony/v1 kind: Solution metadata: @@ -57,7 +58,7 @@ spec: bar: baz: "qux" ` - solution := &solutionv1.Solution{} + solution := &api.Solution{} err := yaml.Unmarshal([]byte(solutionYaml), solution) assert.NoError(t, err) @@ -74,34 +75,101 @@ spec: assert.Equal(t, expectedProperties, actualProperties) } -var _ = BeforeSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - By("bootstrapping test environment") - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: true, - } - - cfg, err := testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - - err = solutionv1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - - //+kubebuilder:scaffold:scheme - - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient).NotTo(BeNil()) - -}) - -var _ = AfterSuite(func() { - Skip("Skipping tests for now as they are no longer relevant") - By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) +// This test is here for legacy reasons. It spins up a test environment +// with a kubernetes api server and etcd. +// It'll continue to live here for now but all integrations tests should be +// created in the /test/integration directory. +// +// Don't write any tests that use the testEnv or k8sClient in this package +// unless there's a strong reason to. +// +// The tests in the target_controller_test.go and instance_controller_test.go +// are now unit tests that use a mocked k8sClient. All behavior that requires +// a manager and full operator pattern should be tested in the integration tests +var _ = Describe("Legacy testing with envtest", Ordered, func() { + var ( + cancel context.CancelFunc + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + apiClient *MockApiClient + ) + + BeforeAll(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseFlagOptions(&zap.Options{Development: true, TimeEncoder: zapcore.ISO8601TimeEncoder}))) + ctx, cancel = context.WithCancel(context.TODO()) + apiClient = &MockApiClient{} + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "oss", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + //+kubebuilder:scaffold:scheme + err = api.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // The default client created by calling client.New behaves slightly different + // from the client created by the manager. The manager's client has preserves + // the type of the object passed to it. The default client does not. So when you + // make a get call with the default client, the object returned doesn't Have the + // GroupVersionKind set. This would cause some certain assersions to fail as some + // objects are prepared and queried outside the reconciler for test assertions + // for this reaseon, we use the manager's client for all tests. + // + // k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + // needs to disable metrics otherwise all controller suite tests will try to bind to the same port (8080) + MetricsBindAddress: "0", + }) + Expect(err).ToNot(HaveOccurred()) + k8sClient = k8sManager.GetClient() + Expect(k8sClient).NotTo(BeNil()) + + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(BuildDefaultTarget(), ""), nil) + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + + err = (&controllers.InstanceReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + ReconciliationInterval: 2 * time.Second, + DeleteTimeOut: 6 * time.Second, + PollInterval: 1 * time.Second, + ApiClient: apiClient, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() + + }) + + AfterAll(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) + }) + + // This doesn't actually test reconcililiation. It only tests that the resources + // can be created in the cluster. The reconciler tests are in the target_controller_test.go and + // instance_controller_test.go files + It("should be able to create valid resources", func() { + By("creating a valid instance") + Expect(k8sClient.Create(ctx, BuildDefaultInstance())).To(Succeed()) + }) }) diff --git a/k8s/main.go b/k8s/main.go index bac9bbfec..07a805471 100644 --- a/k8s/main.go +++ b/k8s/main.go @@ -10,12 +10,17 @@ package main import ( + "context" + "encoding/json" "flag" "fmt" "os" + "time" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -41,8 +46,9 @@ import ( ) var ( - scheme = runtime.NewScheme() - setupLog = ctrl.Log.WithName("setup") + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") + apiCertPath = os.Getenv(constants.ApiCertEnvName) ) func init() { @@ -61,6 +67,14 @@ func main() { var enableLeaderElection bool var probeAddr string var configFile string + var pollIntervalString string + var reconcileIntervalString string + var deleteTimeOutString string + var metricsConfigFile string + var disableWebhooksServer bool + var deleteSyncDelayString string + + flag.StringVar(&metricsConfigFile, "metrics-config-file", "", "The path to the otel metrics config file.") flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -69,6 +83,13 @@ func main() { flag.StringVar(&configFile, "config", "", "The controller will laod its initial configuration from this file. "+ "Omit this flag to use the default configuration value. "+ "Command-line flags override configuration from this file.") + flag.BoolVar(&disableWebhooksServer, "disable-webhooks-server", false, "Whether to disable webhooks server endpoints. ") + flag.StringVar(&pollIntervalString, "poll-interval", "10s", "The interval in seconds to poll the target and instance status during reconciliation.") + flag.StringVar(&reconcileIntervalString, "reconcile-interval", "30m", "The interval in seconds to reconcile the target and instance status.") + // Honor OSS changes: use 1m instead of 5m for delete-timeout + flag.StringVar(&deleteTimeOutString, "delete-timeout", "1m", "The timeout in seconds to wait for the target and instance deletion.") + // Add new settings for delete sync delay + flag.StringVar(&deleteSyncDelayString, "delete-sync-delay", "0s", "The delay in seconds to wait for the status sync back in delete operations.") opts := zap.Options{ Development: true, @@ -80,6 +101,8 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) fmt.Println(constants.EulaMessage) fmt.Println() + + ctx := ctrl.SetupSignalHandler() var err error ctrlConfig := configv1.ProjectConfig{} options := ctrl.Options{ @@ -104,6 +127,59 @@ func main() { os.Exit(1) } + if metricsConfigFile != "" { + obs, err := initMetrics(metricsConfigFile) + if err != nil { + setupLog.Error(err, "unable to initialize metrics") + os.Exit(1) + } + defer shutdownMetrics(obs) + } + + apiClientOptions := []utils.ApiClientOption{ + utils.WithCertAuth(apiCertPath), + } + + if utils.ShouldUseSATokens() { + apiClientOptions = append(apiClientOptions, utils.WithServiceAccountToken()) + } else { + apiClientOptions = append(apiClientOptions, utils.WithUserPassword(ctx, "admin", "")) + } + + apiClient, err := utils.NewAPIClient( + ctx, + utils.GetSymphonyAPIAddressBase(), + apiClientOptions..., + ) + if err != nil { + setupLog.Error(err, "unable to create api client") + os.Exit(1) + } + + pollInterval, err := time.ParseDuration(pollIntervalString) + if err != nil { + setupLog.Error(err, "unable to parse poll interval") + os.Exit(1) + } + + reconcileInterval, err := time.ParseDuration(reconcileIntervalString) + if err != nil { + setupLog.Error(err, "unable to parse reconcile interval") + os.Exit(1) + } + + deleteTimeOut, err := time.ParseDuration(deleteTimeOutString) + if err != nil { + setupLog.Error(err, "unable to parse delete timeout") + os.Exit(1) + } + + deleteSyncDelay, err := time.ParseDuration(deleteSyncDelayString) + if err != nil { + setupLog.Error(err, "unable to parse delete sync delay") + os.Exit(1) + } + if err = (&solutioncontrollers.SolutionReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -126,15 +202,25 @@ func main() { os.Exit(1) } if err = (&solutioncontrollers.InstanceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ReconciliationInterval: reconcileInterval, + DeleteTimeOut: deleteTimeOut, + PollInterval: pollInterval, + DeleteSyncDelay: deleteSyncDelay, + ApiClient: apiClient, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Instance") os.Exit(1) } if err = (&fabriccontrollers.TargetReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ReconciliationInterval: reconcileInterval, + DeleteTimeOut: deleteTimeOut, + PollInterval: pollInterval, + DeleteSyncDelay: deleteSyncDelay, + ApiClient: apiClient, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Target") os.Exit(1) @@ -181,33 +267,35 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Catalog") os.Exit(1) } - if err = (&fabricv1.Device{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Device") - os.Exit(1) - } - if err = (&fabricv1.Target{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Target") - os.Exit(1) - } - if err = (&solutionv1.Solution{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Solution") - os.Exit(1) - } - if err = (&solutionv1.Instance{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Instance") - os.Exit(1) - } - if err = (&aiv1.Model{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Model") - os.Exit(1) - } - if err = (&aiv1.Skill{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Skill") - os.Exit(1) - } - if err = (&federationv1.Catalog{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Catalog") - os.Exit(1) + if !disableWebhooksServer { + if err = (&fabricv1.Device{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Device") + os.Exit(1) + } + if err = (&fabricv1.Target{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Target") + os.Exit(1) + } + if err = (&solutionv1.Solution{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Solution") + os.Exit(1) + } + if err = (&solutionv1.Instance{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Instance") + os.Exit(1) + } + if err = (&aiv1.Model{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Model") + os.Exit(1) + } + if err = (&aiv1.Skill{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Skill") + os.Exit(1) + } + if err = (&federationv1.Catalog{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Catalog") + os.Exit(1) + } } //+kubebuilder:scaffold:builder @@ -221,8 +309,35 @@ func main() { } setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } } + +func initMetrics(configPath string) (*observability.Observability, error) { + // Read file content and parse int ObservabilityConfig + configBytes, err := os.ReadFile(configPath) + if err != nil { + return nil, err + } + + var config observability.ObservabilityConfig + + if err := json.Unmarshal(configBytes, &config); err != nil { + return nil, err + } + + obs := observability.New(constants.K8S) + if err := obs.InitMetric(config); err != nil { + return nil, err + } + + return &obs, nil +} + +func shutdownMetrics(obs *observability.Observability) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + obs.Shutdown(ctx) +} diff --git a/k8s/predicates/operationid.go b/k8s/predicates/operationid.go new file mode 100644 index 000000000..7e135ef0e --- /dev/null +++ b/k8s/predicates/operationid.go @@ -0,0 +1,42 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package predicates + +import ( + "gopls-workspace/constants" + + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// indicates/validates that this type is a predicate +var _ predicate.Predicate = &OperationIdPredicate{} + +type OperationIdPredicate struct { + predicate.Funcs // fills the defaults +} + +// Update implements predicate.Predicate. +func (OperationIdPredicate) Update(e event.UpdateEvent) bool { + if e.ObjectOld == nil { + return false + } + if e.ObjectNew == nil { + return false + } + oldAnnotations := e.ObjectOld.GetAnnotations() + newAnnotations := e.ObjectNew.GetAnnotations() + + var oldOperationId, newOperationId string + if oldAnnotations != nil { + oldOperationId = oldAnnotations[constants.AzureOperationIdKey] + } + if newAnnotations != nil { + newOperationId = newAnnotations[constants.AzureOperationIdKey] + } + return oldOperationId != newOperationId +} diff --git a/k8s/reconcilers/delete_test.go b/k8s/reconcilers/delete_test.go new file mode 100644 index 000000000..00db2718d --- /dev/null +++ b/k8s/reconcilers/delete_test.go @@ -0,0 +1,363 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package reconcilers_test + +import ( + "context" + "errors" + v1 "gopls-workspace/apis/fabric/v1" + "gopls-workspace/reconcilers" + "time" + + . "gopls-workspace/testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("Attempt Delete", func() { + var reconciler *reconcilers.DeploymentReconciler + var apiClient *MockApiClient + var kubeClient client.Client + var object *v1.Target + var reconcileResult reconcile.Result + var reconcileError error + var delayer *MockDelayer + + BeforeEach(func() { + By("building the clients") + apiClient = &MockApiClient{} + kubeClient = CreateFakeKubeClientForFabricGroup( + BuildDefaultTarget(), + ) + delayer = &MockDelayer{} + + By("building the reconciler") + var err error + reconciler, err = reconcilers.NewDeploymentReconciler(append( + DefaultTestReconcilerOptions(), + reconcilers.WithDelayFunc(delayer.Sleep), + reconcilers.WithDeleteSyncDelay(TestDeleteSyncDelay), + reconcilers.WithApiClient(apiClient), + reconcilers.WithClient(kubeClient))..., + ) + Expect(err).NotTo(HaveOccurred()) + }) + + BeforeEach(func(ctx context.Context) { + By("fetching the object from the kubernetes api") + object = &v1.Target{} + Expect(kubeClient.Get(ctx, DefaultTargetNamepsacedName, object)).To(Succeed()) + }) + + BeforeEach(func(ctx context.Context) { + By("adding a finalizer to the object") + object.SetFinalizers([]string{TestFinalizer}) + Expect(kubeClient.Update(ctx, object)).To(Succeed()) + Expect(kubeClient.Get(ctx, DefaultTargetNamepsacedName, object)).To(Succeed()) + Expect(object.GetFinalizers()).To(ContainElement(TestFinalizer)) + }) + + BeforeEach(func() { + By("deleting the object") + Expect(kubeClient.Delete(context.Background(), object)).To(Succeed()) + Expect(kubeClient.Get(context.Background(), DefaultTargetNamepsacedName, object)).To(Succeed()) + }) + + AfterEach(func() { + By("checking that all mocks were called (or not called) with the expected arguments") + apiClient.AssertExpectations(GinkgoT()) + delayer.AssertExpectations(GinkgoT()) + }) + + JustBeforeEach(func(ctx context.Context) { + By("calling the reconciler") + _, reconcileResult, reconcileError = reconciler.AttemptRemove(ctx, object, logr.Discard(), targetOperationStartTimeKey) + }) + + When("the delete timeout has elapsed elapsed", func() { + BeforeEach(func(ctx context.Context) { + By("setting the deletion timestamp to a time in the past") + object.SetDeletionTimestamp(&metav1.Time{Time: time.Now().Add(-TestReconcileTimout)}) + }) + + BeforeEach(func(ctx context.Context) { + By("mocking a delay to allow for deletion error syncing") + delayer.On("Sleep", TestDeleteSyncDelay).Return(nil) + }) + + It("should wait for the deletion to sync", func() { + delayer.AssertExpectations(GinkgoT()) + }) + + It("should have a status of failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + + It("should return a result indicating that the object should not be requeued", func() { + Expect(reconcileResult).To(Equal(reconcile.Result{})) + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + It("should not have a finalizer", func() { + Expect(object.GetFinalizers()).NotTo(ContainElement(TestFinalizer)) + }) + }) + + When("the object has not been queued for deletion on the api but has been deployed", func() { + BeforeEach(func(ctx context.Context) { + By("returning a summary of a deployed but not deleted object from the api") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(object, "test-hash"), nil) + }) + + Context("and it successfully queues a delete job to the api", func() { + BeforeEach(func(ctx context.Context) { + By("returning a successful delete job from the api") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + It("should call the api to get summary and queue a delete job", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of Deleting", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + + It("should return a result indicating that the reconciliation should be requeued within the polling interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestPollInterval)) + Expect(reconcileError).NotTo(HaveOccurred()) + }) + }) + + Context("and it fails to queue a delete job to the api due to a non-terminal error", func() { + BeforeEach(func(ctx context.Context) { + By("returning a non-terminal error from the api") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test-error")) + }) + + It("should call the api to get summary and queue a delete job", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of Deleting", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + + It("should kickoff a reconciliation as soon as possible because of an error", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + }) + + Context("and it fails to queue a delete job to the api due to a terminal error", func() { + BeforeEach(func(ctx context.Context) { + By("returning a terminal error from the api") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(TerminalError) + + By("mocking a delay to allow for deletion error syncing") + delayer.On("Sleep", TestDeleteSyncDelay).Return(nil) + }) + + It("should call the api as expected", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + + It("should wait for the deletion failure status to sync", func() { + delayer.AssertExpectations(GinkgoT()) + }) + + It("should not have a finalizer", func() { + Expect(object.GetFinalizers()).NotTo(ContainElement(TestFinalizer)) + }) + + It("should return a result indicating that the reconciliation should not be requeued", func() { + Expect(reconcileResult).To(Equal(reconcile.Result{})) + Expect(reconcileError).NotTo(HaveOccurred()) + }) + }) + }) + + When("the object has been queued for deletion on the api", func() { + Context("and the delete job is still in progress", func() { + BeforeEach(func(ctx context.Context) { + By("returning an in-progress delete summary from the api") + summary := MockInProgressDeleteSummaryResult(object, "test-hash") + + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should have called the api to get summary with the right arguments", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of Deleting", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + + It("should return a result indicating that the reconciliation should be requeued within the polling interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestPollInterval)) + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + }) + + Context("and the delete job has failed", func() { + BeforeEach(func(ctx context.Context) { + By("returning a failed delete summary from the api") + summary := MockInProgressDeleteSummaryResult(object, "test-hash") + summary.State = model.SummaryStateDone + + By("mocking a delay to allow for deletion error syncing") + delayer.On("Sleep", TestDeleteSyncDelay).Return(nil) + + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should have called the api to get summary with the right arguments", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + + It("should not have a finalizer", func() { + Expect(object.GetFinalizers()).NotTo(ContainElement(TestFinalizer)) + }) + + It("should return a result indicating that the reconciliation should not be requeued", func() { + Expect(reconcileResult).To(Equal(reconcile.Result{})) + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + It("should wait for the deletion failure status to sync", func() { + delayer.AssertExpectations(GinkgoT()) + }) + }) + + Context("and the delete job has succeeded", func() { + BeforeEach(func(ctx context.Context) { + By("returning a successful delete summary from the api") + summary := MockSucessSummaryResult(object, "test-hash") + summary.Summary.IsRemoval = true + + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should have called the api to get summary with the right arguments", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of Succeeded", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Succeeded")) + }) + + It("should not have a finalizer", func() { + Expect(object.GetFinalizers()).NotTo(ContainElement(TestFinalizer)) + }) + + It("should return a result indicating that the reconciliation should not be requeued", func() { + Expect(reconcileResult.RequeueAfter).To(BeZero()) + Expect(reconcileError).NotTo(HaveOccurred()) + }) + }) + }) + + When("the delete job summary cannot be fetched from the api due to random error", func() { + BeforeEach(func(ctx context.Context) { + By("returning an error from the get summary api endpoint") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("test-error")) + }) + + It("should have called the api to get summary with the right arguments", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should kickoff another reconciliation as soon as possible because of an error", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + + It("should have a status of Deleting", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + }) + + When("the delete job summary cannot be fetched from the api due to not found", func() { + BeforeEach(func(ctx context.Context) { + By("returning an error from the get summary api endpoint") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + }) + + Context("so it successfully queues a delete job to the api", func() { + BeforeEach(func(ctx context.Context) { + By("mocking a successful call to the queue delete job api endpoint") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + It("should have called the api to get summary and queue a job", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + It("should have a status of Deleting", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + }) + + Context("so it fails to queue a delete job to the api due to a non-terminal error", func() { + BeforeEach(func(ctx context.Context) { + By("mocking a non-terminal error from the queue delete job api endpoint") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test-error")) + }) + + It("should have called the api to get summary and queue a job", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should return an error", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + + It("should have a status of Deleting", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Deleting")) + }) + }) + }) + + When("the api returns a summary in a pending state", func() { + BeforeEach(func(ctx context.Context) { + By("returning a pending summary from the api") + summary := MockInProgressDeleteSummaryResult(object, "test-hash") + summary.State = model.SummaryStatePending + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should have called the api to get summary with the right arguments", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should return a result indicating that the reconciliation should be requeued within the polling interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestPollInterval)) + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + }) +}) diff --git a/k8s/reconcilers/deployment.go b/k8s/reconcilers/deployment.go new file mode 100644 index 000000000..f53dc691f --- /dev/null +++ b/k8s/reconcilers/deployment.go @@ -0,0 +1,689 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package reconcilers + +import ( + "context" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "time" + + k8smodel "gopls-workspace/apis/model/v1" + "gopls-workspace/constants" + "gopls-workspace/controllers/metrics" + "gopls-workspace/utils" + + utilsmodel "gopls-workspace/utils/model" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/go-logr/logr" + + "github.com/google/uuid" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type ( + patchStatusOptions struct { + deploymentQueued bool + nonTerminalErr error + terminalErr error + } + DeploymentReconciler struct { + finalizerName string + kubeClient client.Client + apiClient utils.ApiClient + reconciliationInterval time.Duration + pollInterval time.Duration + deleteTimeOut time.Duration + applyTimeOut time.Duration // TODO: Use reconciliation policy to configure + deleteSyncDelay time.Duration // TODO: Use operator reconcile loop instead of this delay + delayFunc func(time.Duration) + deploymentKeyResolver func(Reconcilable) string + deploymentErrorBuilder func(*apimodel.SummaryResult, error, *apimodel.ErrorType) + deploymentBuilder func(ctx context.Context, object Reconcilable) (*apimodel.DeploymentSpec, error) + } + DeploymentReconcilerOptions func(*DeploymentReconciler) + ReconcilerSubject string +) + +const ( + defaultTimeout = 15 * time.Minute + defaultReconciliationInterval = 30 * time.Minute + defaultPollInterval = 10 * time.Second +) + +var ( + _ Reconciler = &DeploymentReconciler{} + termialErrors = map[v1alpha2.State]struct{}{ + v1alpha2.TimedOut: {}, + v1alpha2.TargetPropertyNotFound: {}, + } +) + +func NewDeploymentReconciler(opts ...DeploymentReconcilerOptions) (*DeploymentReconciler, error) { + r := &DeploymentReconciler{ + deploymentKeyResolver: defaultDeploymentKeyResolver, + deploymentErrorBuilder: defaultProvisioningErrorBuilder, + delayFunc: time.Sleep, + applyTimeOut: defaultTimeout, + reconciliationInterval: defaultReconciliationInterval, + pollInterval: defaultPollInterval, + deleteTimeOut: defaultTimeout, + } + for _, opt := range opts { + opt(r) + } + if r.finalizerName == "" { + return nil, fmt.Errorf("finalizer name cannot be empty") + } + if r.kubeClient == nil { + return nil, fmt.Errorf("kube client cannot be nil") + } + if r.apiClient == nil { + return nil, fmt.Errorf("api client cannot be nil") + } + if r.deploymentBuilder == nil { + return nil, fmt.Errorf("deployment builder cannot be nil") + } + return r, nil +} + +func (r *DeploymentReconciler) deriveReconcileInterval(log logr.Logger, target Reconcilable) (reconciliationInterval, timeout time.Duration) { + rp := target.GetReconciliationPolicy() + reconciliationInterval = r.reconciliationInterval + timeout = r.applyTimeOut + if rp != nil { + // reconciliationPolicy is set, use the interval if it's active + if rp.State.IsActive() { + // periodic reconciliation, interval is set + if rp.Interval != nil { + interval, err := time.ParseDuration(*rp.Interval) + if err != nil { + log.Info(fmt.Sprintf("failed to parse reconciliation interval %s, using default %s", *rp.Interval, reconciliationInterval)) + return + } + reconciliationInterval = interval + } + } + if rp.State.IsInActive() { + // only reconcile once + reconciliationInterval = 0 + } + + } + // no reconciliationPolicy configured or reconciliationPolicy.state is invalid, use default reconciliation interval: r.reconciliationInterval + return +} + +// attemptUpdate attempts to update the instance +func (r *DeploymentReconciler) AttemptUpdate(ctx context.Context, object Reconcilable, log logr.Logger, operationStartTimeKey string) (metrics.OperationStatus, reconcile.Result, error) { + if !controllerutil.ContainsFinalizer(object, r.finalizerName) { + controllerutil.AddFinalizer(object, r.finalizerName) + // updates the object in Kubernetes cluster + if err := r.kubeClient.Update(ctx, object); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + } + + if object.GetAnnotations()[operationStartTimeKey] == "" || utilsmodel.IsTerminalState(object.GetStatus().ProvisioningStatus.Status) { + r.patchOperationStartTime(object, operationStartTimeKey) + if err := r.kubeClient.Update(ctx, object); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + } + + // Get reconciliation interval + reconciliationInterval, timeout := r.deriveReconcileInterval(log, object) + + // If the object hasn't reached a terminal state and the time since the operation started is greater than the + // apply timeout, we should update the status with a terminal error and return + startTime, err := time.Parse(time.RFC3339, object.GetAnnotations()[operationStartTimeKey]) + if err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + if time.Since(startTime) > timeout && !utilsmodel.IsTerminalState(object.GetStatus().ProvisioningStatus.Status) { + if _, err := r.updateObjectStatus(ctx, object, nil, patchStatusOptions{ + terminalErr: v1alpha2.NewCOAError(nil, "failed to completely reconcile within the allocated time", v1alpha2.TimedOut), + }, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.DeploymentTimedOut, ctrl.Result{RequeueAfter: reconciliationInterval}, nil + } + + summary, err := r.getDeploymentSummary(ctx, object) + if err != nil { + // If the error is anything but 404, we should return the error so the reconciler can retry + if !v1alpha2.IsNotFound(err) { + // updates the object status to reconciling + if _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{ + nonTerminalErr: err, + }, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.GetDeploymentSummaryFailed, ctrl.Result{}, err + } else { + // It's not found in api so we should mark as reconciling, queue a job and check back in POLL seconds + if err := r.queueDeploymentJob(ctx, object, false, false, operationStartTimeKey); err != nil { + return r.handleDeploymentError(ctx, object, summary, reconciliationInterval, err, log) + } + + if _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{deploymentQueued: true}, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.DeploymentQueued, ctrl.Result{RequeueAfter: r.pollInterval}, nil + } + } + + switch summary.State { + case apimodel.SummaryStatePending: + // do nothing and check back in POLL seconds + return metrics.StatusNoOp, ctrl.Result{RequeueAfter: r.pollInterval}, nil + case apimodel.SummaryStateRunning: + // if there is a parity mismatch between the object and the summary, the api is probably busy reconciling + // a previous revision, so we'll only make sure the status is Non-terminal + // But if they are the same, it's currently reconciling this generatation + // we'll update the status and also the current progress. Either way, we'll check back in POLL seconds + if _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{}, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.DeploymentStatusPolled, ctrl.Result{RequeueAfter: r.pollInterval}, nil + case apimodel.SummaryStateDone: + // If the generation doesn't match the current generation, it means the api finished reconciling a previous + // generation so we need to queue a new job and check back in POLL seconds. Due to current limitations in the + // api, if the api is currently busy reconciling a different object, it will successfully queue this job but + // the api would not send a summary object back. This means we might queue multiple jobs for the same generation + // but it's better than not queueing a job at all. + if !r.hasParity(ctx, object, summary, log) { + if err = r.queueDeploymentJob(ctx, object, false, true, operationStartTimeKey); err != nil { + return r.handleDeploymentError(ctx, object, summary, reconciliationInterval, err, log) + } + + if _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{deploymentQueued: true}, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.DeploymentQueued, ctrl.Result{RequeueAfter: r.pollInterval}, nil + } + + // There's parity, so we should update the status to a terminal state and proceed based on the reconcile policy + if _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{}, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + + // If the reconcile policy is once (interval == 0 or state==inactive), we should not queue a new job and return + if reconciliationInterval == 0 { + return metrics.DeploymentSucceeded, ctrl.Result{}, nil + } + + // The reconcile policy is periodic (interval > 0 and state == active). We should check if the difference + // in time between the summary time and the current time is greater than the reconciliation interval + // If it is, we should queue a new job to the api and check back in POLL seconds + // else we should queue a reconciliation and check back in the difference between the summary time and the current time + if time.Since(summary.Time) > reconciliationInterval { + if err = r.queueDeploymentJob(ctx, object, false, true, operationStartTimeKey); err != nil { + return r.handleDeploymentError(ctx, object, summary, reconciliationInterval, err, log) + } + + if _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{deploymentQueued: true}, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.DeploymentQueued, ctrl.Result{RequeueAfter: r.pollInterval}, nil + } + return metrics.DeploymentSucceeded, ctrl.Result{RequeueAfter: reconciliationInterval - time.Since(summary.Time)}, nil + default: + return metrics.StatusNoOp, ctrl.Result{}, fmt.Errorf("should not reach here") + } +} + +// attemptRemove attempts to remove the object +func (r *DeploymentReconciler) AttemptRemove(ctx context.Context, object Reconcilable, log logr.Logger, operationStartTimeKey string) (metrics.OperationStatus, reconcile.Result, error) { + status := metrics.StatusNoOp + if !controllerutil.ContainsFinalizer(object, r.finalizerName) { + return metrics.StatusNoOp, ctrl.Result{}, nil + } + + // Timeout will be deletion timestamp + delete timeout duration + timeout := object.GetDeletionTimestamp().Time.Add(r.deleteTimeOut) + + if metav1.Now().Time.After(timeout) { + // If the timeout has been reached, Update the status with a terminal error and remove finalizer after a brief delay + // so that ARM can sycnchroniize the failure + r.updateObjectStatus(ctx, object, nil, patchStatusOptions{ + terminalErr: v1alpha2.NewCOAError(nil, "failed to completely delete the resource within the allocated time", v1alpha2.TimedOut), + }, log) + r.delayFunc(r.deleteSyncDelay) + return metrics.DeploymentTimedOut, ctrl.Result{}, r.concludeDeletion(ctx, object) + } + + // Grab summary + summary, err := r.getDeploymentSummary(ctx, object) + // If there was an error and it was not a 404, we should update the status and return the error so the reconciler can retry + if err != nil && !v1alpha2.IsNotFound(err) { + if _, uErr := r.updateObjectStatus(ctx, object, nil, patchStatusOptions{nonTerminalErr: err}, log); uErr != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, uErr + } + return metrics.GetDeploymentSummaryFailed, ctrl.Result{}, err + } + + // Since the summary is not found, we should queue a job and check back in POLL seconds + if err != nil { + if err = r.queueDeploymentJob(ctx, object, true, false, operationStartTimeKey); err != nil { + return r.handleDeleteDeploymentError(ctx, object, summary, err, log) + } + if _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{deploymentQueued: true}, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.DeploymentQueued, ctrl.Result{RequeueAfter: r.pollInterval}, nil + } + + switch summary.State { + case apimodel.SummaryStatePending: + // do nothing and check back in POLL seconds + return metrics.StatusNoOp, ctrl.Result{RequeueAfter: r.pollInterval}, nil + case apimodel.SummaryStateRunning: + // if there is a parity mismatch between the object and the summary, the api is probably busy reconciling + // a previous revision, so we'll only make sure the status is Non-terminal + // But if they are the same, it's currently reconciling this generatation + // we'll update the status and also the current progress. Either way, we'll check back in POLL seconds + if _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{}, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.DeploymentStatusPolled, ctrl.Result{RequeueAfter: r.pollInterval}, nil + case apimodel.SummaryStateDone: + // If the generation doesn't match the current generation, it means the api finished reconciling a previous + // generation so we need to queue a new job and check back in POLL seconds. Due to current limitations in the + // api, if the api is currently busy reconciling a different object, it will successfully queue this job but + // the api would not send a summary object back. This means we might queue multiple jobs for the same generation + // but it's better than not queueing a job at all. + if !r.hasParity(ctx, object, summary, log) { + if err = r.queueDeploymentJob(ctx, object, true, true, operationStartTimeKey); err != nil { + return r.handleDeleteDeploymentError(ctx, object, summary, err, log) + } + + // We've queued a job so we should update the status and check back in POLL seconds + if _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{deploymentQueued: true}, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.DeploymentQueued, ctrl.Result{RequeueAfter: r.pollInterval}, nil + } + + // There's parity, so we should update the status to a terminal state and conclude the deletion + _, err := r.updateObjectStatus(ctx, object, summary, patchStatusOptions{}, log) + if err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + + if object.GetStatus().ProvisioningStatus.Status == string(utilsmodel.ProvisioningStatusFailed) { + r.delayFunc(r.deleteSyncDelay) + } + if err := r.concludeDeletion(ctx, object); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + return metrics.DeploymentSucceeded, ctrl.Result{}, nil + default: + return status, ctrl.Result{}, fmt.Errorf("should not reach here") + } +} + +func (r *DeploymentReconciler) handleDeploymentError(ctx context.Context, object Reconcilable, summary *model.SummaryResult, reconcileInterval time.Duration, err error, log logr.Logger) (metrics.OperationStatus, ctrl.Result, error) { + patchOptions := patchStatusOptions{} + if isTermnalError(err, termialErrors) { + patchOptions.terminalErr = err + } else { + patchOptions.nonTerminalErr = err + } + + // update the object status + if _, err = r.updateObjectStatus(ctx, object, summary, patchOptions, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + + // If there was a terminal error, then we don't return an error so the reconciler can respect the reconcile policy + // but if there was a non-terminal error, we should return the error so the reconciler can retry + if patchOptions.terminalErr != nil { + return metrics.DeploymentFailed, ctrl.Result{RequeueAfter: reconcileInterval}, nil + } + return metrics.QueueDeploymentFailed, ctrl.Result{}, patchOptions.nonTerminalErr +} +func (r *DeploymentReconciler) handleDeleteDeploymentError(ctx context.Context, object Reconcilable, summary *model.SummaryResult, err error, log logr.Logger) (metrics.OperationStatus, ctrl.Result, error) { + patchOptions := patchStatusOptions{} + if isTermnalError(err, termialErrors) { + patchOptions.terminalErr = err + } else { + patchOptions.nonTerminalErr = err + } + + // update the object status + if _, err = r.updateObjectStatus(ctx, object, summary, patchOptions, log); err != nil { + return metrics.StatusUpdateFailed, ctrl.Result{}, err + } + + // If there was a terminal error, then we want to conclude the deletion + // but give the api a chance to synchronize the failure before removing the finalizer + if patchOptions.terminalErr != nil { + r.delayFunc(r.deleteSyncDelay) + return metrics.DeploymentFailed, ctrl.Result{}, r.concludeDeletion(ctx, object) + } + return metrics.QueueDeploymentFailed, ctrl.Result{}, patchOptions.nonTerminalErr +} + +func (r *DeploymentReconciler) concludeDeletion(ctx context.Context, object Reconcilable) error { + controllerutil.RemoveFinalizer(object, r.finalizerName) + if err := r.kubeClient.Update(ctx, object); err != nil { + return err + } + return nil +} + +func (r *DeploymentReconciler) hasParity(ctx context.Context, object Reconcilable, summary *model.SummaryResult, log logr.Logger) bool { + if object == nil || summary == nil { // we don't expect any of these to be nil + return false + } + generationMatch := r.generationMatch(object, summary) + operationTypeMatch := r.operationTypeMatch(object, summary) + deploymentHashMatch := r.deploymentHashMatch(ctx, object, summary) + log.Info(fmt.Sprintf("CheckParity: generationMatch: %t, operationTypeMatch: %t, deploymentHashMatch: %t", generationMatch, operationTypeMatch, deploymentHashMatch)) + return generationMatch && operationTypeMatch && deploymentHashMatch +} + +func (r *DeploymentReconciler) generationMatch(object Reconcilable, summary *model.SummaryResult) bool { + if object == nil || summary == nil { // we don't expect any of these to be nil + return false + } + return summary.Generation == strconv.FormatInt(object.GetGeneration(), 10) +} + +func (r *DeploymentReconciler) operationTypeMatch(object Reconcilable, summary *model.SummaryResult) bool { + if object == nil || summary == nil { // we don't expect any of these to be nil + return false + } + if summary.Summary.IsRemoval { + return object.GetDeletionTimestamp() != nil + } + return object.GetDeletionTimestamp() == nil +} + +func (r *DeploymentReconciler) deploymentHashMatch(ctx context.Context, object Reconcilable, summary *model.SummaryResult) bool { + if object == nil || summary == nil { // we don't expect any of these to be nil + return false + } + deployment, err := r.deploymentBuilder(ctx, object) + if err != nil { + return false + } + return summary.DeploymentHash == deployment.Hash +} + +func (r *DeploymentReconciler) queueDeploymentJob(ctx context.Context, object Reconcilable, isRemoval bool, updateCorrelationId bool, operationStartTimeKey string) error { + // If previous status was terminal and there is no parity between the summary and current object, then update correlation id. + // This will ensure that there is a new correlation id between deployments including deployments that periodically occur. + if updateCorrelationId && utilsmodel.IsTerminalState(object.GetStatus().ProvisioningStatus.Status) { + r.updateCorrelationIdMetaData(ctx, object, operationStartTimeKey) + } + + // Build the deployment object to send to the api + deployment, err := r.deploymentBuilder(ctx, object) + if err != nil { + return err + } + + // Send the deployment object to the api to queue a job + err = r.apiClient.QueueDeploymentJob(ctx, object.GetNamespace(), isRemoval, *deployment) + if err != nil { + return err + } + return nil +} + +func (r *DeploymentReconciler) getDeploymentSummary(ctx context.Context, object Reconcilable) (*model.SummaryResult, error) { + return r.apiClient.GetSummary(ctx, r.deploymentKeyResolver(object), object.GetNamespace()) +} + +func (r *DeploymentReconciler) updateCorrelationIdMetaData(ctx context.Context, object Reconcilable, operationStartTimeKey string) error { + correlationId := uuid.New() + r.patchOperationStartTime(object, operationStartTimeKey) + object.GetAnnotations()[constants.AzureCorrelationId] = correlationId.String() + if err := r.kubeClient.Update(ctx, object); err != nil { + return err + } + + return nil +} + +func (r *DeploymentReconciler) patchOperationStartTime(object Reconcilable, operationStartTimeKey string) { + annotations := object.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations[operationStartTimeKey] = time.Now().Format(time.RFC3339) + object.SetAnnotations(annotations) +} + +func (r *DeploymentReconciler) ensureOperationState(annotations map[string]string, objectStatus *k8smodel.DeployableStatus, provisioningState string) { + objectStatus.ProvisioningStatus.Status = provisioningState + objectStatus.ProvisioningStatus.OperationID = annotations[constants.AzureOperationIdKey] +} + +func (r *DeploymentReconciler) updateObjectStatus(ctx context.Context, object Reconcilable, summaryResult *model.SummaryResult, opts patchStatusOptions, log logr.Logger) (provisioningState string, err error) { + status := r.determineProvisioningStatus(ctx, object, summaryResult, opts, log) + originalStatus := object.GetStatus() + nextStatus := originalStatus.DeepCopy() + + r.patchBasicStatusProps(ctx, object, summaryResult, status, nextStatus, opts, log) + r.patchComponentStatusReport(ctx, object, summaryResult, nextStatus, log) + r.updateProvisioningStatus(ctx, object, summaryResult, status, nextStatus, opts, log) + + if reflect.DeepEqual(&originalStatus, nextStatus) { + return string(status), nil + } + nextStatus.LastModified = metav1.Now() + object.SetStatus(*nextStatus) + return string(status), r.kubeClient.Status().Update(context.Background(), object) +} + +func (r *DeploymentReconciler) determineProvisioningStatus(ctx context.Context, object Reconcilable, summaryResult *model.SummaryResult, opts patchStatusOptions, log logr.Logger) utilsmodel.ProvisioningStatus { + if opts.terminalErr != nil { + // add more details of the terminal error to the status + return utilsmodel.ProvisioningStatusFailed + } + + if opts.nonTerminalErr != nil || summaryResult == nil || !r.hasParity(ctx, object, summaryResult, log) || opts.deploymentQueued { + return utilsmodel.GetNonTerminalStatus(object) + } + + summary := summaryResult.Summary + switch summaryResult.State { + case model.SummaryStateDone: + // Honor OSS changes: https://github.com/eclipse-symphony/symphony/pull/148 + // Use AllAssignedDeployed instead of targetCount/successCount to verify deployment. + status := utilsmodel.ProvisioningStatusSucceeded + if !summary.AllAssignedDeployed { + status = utilsmodel.ProvisioningStatusFailed + } + return status + default: + return utilsmodel.GetNonTerminalStatus(object) + } +} + +func (r *DeploymentReconciler) patchBasicStatusProps(ctx context.Context, object Reconcilable, summaryResult *model.SummaryResult, status utilsmodel.ProvisioningStatus, objectStatus *k8smodel.DeployableStatus, opts patchStatusOptions, log logr.Logger) { + if objectStatus.Properties == nil { + objectStatus.Properties = make(map[string]string) + } + defer func() { // keeping for backward compatibility. Ideally we should remove this and use the provisioning status and provisioning status output + objectStatus.Properties["status"] = string(status) + if opts.nonTerminalErr != nil { + objectStatus.Properties["status-details"] = fmt.Sprintf("%s: due to %s", status, opts.nonTerminalErr.Error()) + } + }() + + if opts.terminalErr != nil { + objectStatus.Properties["deployed"] = "failed" + objectStatus.Properties["targets"] = "failed" + objectStatus.Properties["status-details"] = opts.terminalErr.Error() + return + } + + if summaryResult == nil || !r.hasParity(ctx, object, summaryResult, log) { + objectStatus.Properties["deployed"] = "pending" + objectStatus.Properties["targets"] = "pending" + objectStatus.Properties["status-details"] = "" + return + } + + summary := summaryResult.Summary + targetCount := strconv.Itoa(summary.TargetCount) + successCount := strconv.Itoa(summary.SuccessCount) + + objectStatus.Properties["deployed"] = successCount + objectStatus.Properties["targets"] = targetCount + objectStatus.Properties["status-details"] = summary.SummaryMessage +} + +func (r *DeploymentReconciler) patchComponentStatusReport(ctx context.Context, object Reconcilable, summaryResult *model.SummaryResult, objectStatus *k8smodel.DeployableStatus, log logr.Logger) { + if objectStatus.Properties == nil { + return + } + // If a component is ever deployed, it will always show in Status.Properties + // If a component is not deleted, it will first be reset to Untouched and + // then changed to corresponding status later + for k, v := range objectStatus.Properties { + // Check status prefix (e.g. Deleted -) since status ends with a "-" + if utils.IsComponentKey(k) && !strings.HasPrefix(v, v1alpha2.Deleted.String()) { + objectStatus.Properties[k] = v1alpha2.Untouched.String() + } + } + if summaryResult == nil || !r.hasParity(ctx, object, summaryResult, log) { + return + } + summary := summaryResult.Summary + // Change to corresponding status + // TargetResults should be empty if there a successful deletion + for k, v := range summary.TargetResults { + objectStatus.Properties["targets."+k] = fmt.Sprintf("%s - %s", v.Status, v.Message) + for kc, c := range v.ComponentResults { + if c.Message == "" { + // Honor OSS changes: https://github.com/eclipse-symphony/symphony/pull/225 + // If c.Message is empty, only show c.Status. + objectStatus.Properties["targets."+k+"."+kc] = c.Status.String() + } else { + objectStatus.Properties["targets."+k+"."+kc] = fmt.Sprintf("%s - %s", c.Status, c.Message) + } + } + } +} + +func (r *DeploymentReconciler) updateProvisioningStatus(ctx context.Context, object Reconcilable, summaryResult *model.SummaryResult, provisioningStatus utilsmodel.ProvisioningStatus, objectStatus *k8smodel.DeployableStatus, opts patchStatusOptions, log logr.Logger) { + // THIS IS A HACK. to align with legacy expectations, we need to concatenate + // the status with the non-terminal error message. This is not ideal and should + // be removed in the future + var statusText string = string(provisioningStatus) + if opts.nonTerminalErr != nil { + statusText = fmt.Sprintf("%s: due to %s", provisioningStatus, opts.nonTerminalErr.Error()) + } + r.ensureOperationState(object.GetAnnotations(), objectStatus, statusText) + + // Start with a clean Error object and update all the fields + objectStatus.ProvisioningStatus.Error = apimodel.ErrorType{} + // Output field is updated if status is Succeeded + objectStatus.ProvisioningStatus.Output = make(map[string]string) + + if provisioningStatus == utilsmodel.ProvisioningStatusFailed { + errorObj := &objectStatus.ProvisioningStatus.Error + + // Fill error details into error object + err := opts.nonTerminalErr + if opts.terminalErr != nil { + err = opts.terminalErr + + } + + r.deploymentErrorBuilder(summaryResult, err, errorObj) + return + } + + if summaryResult == nil || !r.hasParity(ctx, object, summaryResult, log) { + return + } + summary := summaryResult.Summary + + outputMap := objectStatus.ProvisioningStatus.Output + // Fill component details into output field + for k, v := range summary.TargetResults { + for ck, cv := range v.ComponentResults { + outputMap[fmt.Sprintf("%s.%s", k, ck)] = cv.Status.String() + } + } + if len(outputMap) == 0 { + objectStatus.ProvisioningStatus.Output = nil + } +} + +func defaultDeploymentKeyResolver(object Reconcilable) string { + return object.GetName() +} + +func defaultProvisioningErrorBuilder(summaryResult *model.SummaryResult, err error, errorObj *apimodel.ErrorType) { + // Fill error details into error object + errorObj.Code = "Symphony: [500]" + + if summaryResult != nil { + summary := summaryResult.Summary + + if summary.IsRemoval { + errorObj.Message = fmt.Sprintf("Uninstall failed. %s", summary.SummaryMessage) + } else { + errorObj.Message = fmt.Sprintf("Deployment failed. %s", summary.SummaryMessage) + } + + errorObj.Target = "Symphony" + errorObj.Details = make([]apimodel.TargetError, 0) + for k, v := range summary.TargetResults { + targetObject := apimodel.TargetError{ + Code: v.Status, + Message: v.Message, + Target: k, + Details: make([]apimodel.ComponentError, 0), + } + for ck, cv := range v.ComponentResults { + targetObject.Details = append(targetObject.Details, apimodel.ComponentError{ + Code: cv.Status.String(), + Message: cv.Message, + Target: ck, + }) + } + errorObj.Details = append(errorObj.Details, targetObject) + } + } + + if err != nil { + errorObj.Message = fmt.Sprintf("%s, %s", err.Error(), errorObj.Message) + } +} + +// checks if the error is terminal +func isTermnalError(err error, terminalErrors map[v1alpha2.State]struct{}) bool { + if err == nil { + return false + } + + var coaErr v1alpha2.COAError + if errors.As(err, &coaErr) { + _, ok := terminalErrors[coaErr.State] + return ok + } + + return false +} diff --git a/k8s/reconcilers/options.go b/k8s/reconcilers/options.go new file mode 100644 index 000000000..4eb117200 --- /dev/null +++ b/k8s/reconcilers/options.go @@ -0,0 +1,82 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package reconcilers + +import ( + "context" + "gopls-workspace/utils" + "time" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func WithFinalizerName(name string) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.finalizerName = name + } +} + +func WithClient(c client.Client) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.kubeClient = c + } +} + +func WithApiClient(c utils.ApiClient) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.apiClient = c + } +} + +func WithReconciliationInterval(d time.Duration) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.reconciliationInterval = d + } +} + +func WithPollInterval(d time.Duration) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.pollInterval = d + } +} + +func WithDeleteTimeOut(d time.Duration) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.deleteTimeOut = d + } +} + +func WithDeleteSyncDelay(d time.Duration) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.deleteSyncDelay = d + } +} + +func WithDelayFunc(f func(time.Duration)) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.delayFunc = f + } +} + +func WithDeploymentKeyResolver(f func(Reconcilable) string) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.deploymentKeyResolver = f + } +} + +func WithDeploymentErrorBuilder(f func(*model.SummaryResult, error, *model.ErrorType)) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.deploymentErrorBuilder = f + } +} + +func WithDeploymentBuilder(f func(ctx context.Context, object Reconcilable) (*model.DeploymentSpec, error)) DeploymentReconcilerOptions { + return func(r *DeploymentReconciler) { + r.deploymentBuilder = f + } +} diff --git a/k8s/reconcilers/policies_test.go b/k8s/reconcilers/policies_test.go new file mode 100644 index 000000000..6d9c61c62 --- /dev/null +++ b/k8s/reconcilers/policies_test.go @@ -0,0 +1,544 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package reconcilers_test + +import ( + "context" + "errors" + v1 "gopls-workspace/apis/fabric/v1" + k8smodel "gopls-workspace/apis/model/v1" + "gopls-workspace/reconcilers" + + . "gopls-workspace/testing" + "time" + + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("Reconcile Policies", func() { + var reconciler *reconcilers.DeploymentReconciler + var apiClient *MockApiClient + var kubeClient client.Client + var object *v1.Target + var reconcileResult reconcile.Result + var reconcileError error + + BeforeEach(func() { + By("building the clients") + apiClient = &MockApiClient{} + kubeClient = CreateFakeKubeClientForFabricGroup( + BuildDefaultTarget(), + ) + + By("building the reconciler") + var err error + reconciler, err = reconcilers.NewDeploymentReconciler(append( + DefaultTestReconcilerOptions(), + reconcilers.WithApiClient(apiClient), + reconcilers.WithClient(kubeClient))..., + ) + Expect(err).NotTo(HaveOccurred()) + }) + + BeforeEach(func(ctx context.Context) { + By("fetching the latest object") + object = &v1.Target{} + err := kubeClient.Get(ctx, DefaultTargetNamepsacedName, object) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func(ctx context.Context) { + By("asserting that mocks were called exactly as expected") + apiClient.AssertExpectations(GinkgoT()) + }) + + JustBeforeEach(func(ctx context.Context) { + By("calling the reconciler") + _, reconcileResult, reconcileError = reconciler.AttemptUpdate(ctx, object, logr.Discard(), targetOperationStartTimeKey) + }) + + Context("object has invalid reconcile policy", func() { + When("reconcile policy state is invalid", func() { + BeforeEach(func(ctx context.Context) { + By("updating the object with an invalid reconcile policy") + object.Spec.ReconciliationPolicy = &k8smodel.ReconciliationPolicySpec{State: "invalid"} + err := kubeClient.Update(ctx, object) + Expect(err).NotTo(HaveOccurred()) + }) + + BeforeEach(func() { + By("mocking the summary response with a successful deployment") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(object, "test-hash"), nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should fall back to default reconciliation interval", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + It("should requue after some time", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestReconcileInterval)) + }) + }) + + When("reconcile policy state is valid but interval is invalid", func() { + BeforeEach(func(ctx context.Context) { + By("updating the object with an invalid reconcile policy") + object.Spec.ReconciliationPolicy = &k8smodel.ReconciliationPolicySpec{State: "active", Interval: ToPointer("invalid")} + err := kubeClient.Update(ctx, object) + Expect(err).NotTo(HaveOccurred()) + }) + + BeforeEach(func() { + By("mocking the summary response with a successful deployment") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(object, "test-hash"), nil) + }) + + It("should fall back to default reconciliation interval", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + It("should requue after some time", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestReconcileInterval)) + }) + }) + }) + + Context("object has no reconcile policy", func() { + // use the default reconcile interval + BeforeEach(func(ctx context.Context) { + By("updating the object with no reconcile policy") + object.Spec.ReconciliationPolicy = nil + err := kubeClient.Update(ctx, object) + Expect(err).NotTo(HaveOccurred()) + }) + + BeforeEach(func() { + By("mocking the summary response with a successful deployment") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(object, "test-hash"), nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should fall back to default reconciliation interval", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + It("should requue after some time", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestReconcileInterval)) + }) + }) + + Context("object has valid reconcile policy", func() { + When("reconcile policy state is active", func() { + BeforeEach(func(ctx context.Context) { + By("updating the object with a valid reconcile policy") + object.Spec.ReconciliationPolicy = &k8smodel.ReconciliationPolicySpec{State: "active", Interval: ToPointer("1m")} + err := kubeClient.Update(ctx, object) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("deployment is not queued to the api due to non-terminal error", func() { + BeforeEach(func() { + By("mocking the summary response with not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking the queue deployment response with a non-terminal error") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test error")) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should re-queue a reconcile job immediately due to non-terminal error", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + It("should have a status of reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + + Context("deployment is not queued to the api due to terminal error", func() { + BeforeEach(func() { + By("mocking the summary response with not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking the queue deployment response with a terminal error") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(TerminalError) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should have a status of failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + It("should queue a reconcile job after reconcile interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(time.Minute)) + }) + }) + + Context("deployment job queued successfully", func() { + BeforeEach(func() { + By("mocking the summary response with not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking the queue deployment response with a successful deployment") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should queue a reconcile job to poll for status", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestPollInterval)) + }) + }) + + Context("deployment to api is completed successful", func() { + BeforeEach(func() { + By("mocking the summary response with a successful deployment") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(object, "test-hash"), nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should return not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should queue a reconcile job after reconcile interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(time.Minute)) + }) + + It("should have a status of Succeeded", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Succeeded")) + }) + }) + Context("deployment to api is completed with failure", func() { + BeforeEach(func() { + By("mocking the summary response with a failed deployment") + summary := MockSucessSummaryResult(object, "test-hash") + summary.Summary.SuccessCount = 0 + summary.Summary.AllAssignedDeployed = false + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should return not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should queue a reconcile job after reconcile interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(time.Minute)) + }) + + It("should have a status of Failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + }) + }) + + When("reconcile policy type is once", func() { + BeforeEach(func(ctx context.Context) { + By("updating the object with a valid reconcile policy") + object.Spec.ReconciliationPolicy = &k8smodel.ReconciliationPolicySpec{State: "active", Interval: ToPointer("0s")} + err := kubeClient.Update(ctx, object) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("deployment is not queued to the api due to non-terminal error", func() { + BeforeEach(func() { + By("mocking the summary response with not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking the queue deployment response with a non-terminal error") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test error")) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should re-queue a reconcile job immediately", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + It("should have a status of reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + + Context("deployment is not queued to the api due to terminal error", func() { + + BeforeEach(func() { + By("mocking the summary response with not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking the queue deployment response with a terminal error") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(TerminalError) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + It("should not queue a reconcile job because the reconcile session is done", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + Expect(reconcileResult.Requeue).To(BeFalse()) + Expect(reconcileResult.RequeueAfter).To(BeZero()) + }) + }) + + Context("deployment job queued successfully", func() { + BeforeEach(func() { + By("mocking the summary response with not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking the queue deployment response with a successful deployment") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should queue a reconcile job to poll for status", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestPollInterval)) + }) + + It("should have a status of reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + + Context("deployment to api is completed successful", func() { + BeforeEach(func() { + By("mocking the summary response with a successful deployment") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(object, "test-hash"), nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of succeeded", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Succeeded")) + }) + + It("should not queue a reconcile job because the reconcile session is done", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + Expect(reconcileResult.Requeue).To(BeFalse()) + Expect(reconcileResult.RequeueAfter).To(BeZero()) + }) + }) + Context("deployment to api is completed with failure", func() { + BeforeEach(func() { + By("mocking the summary response with a failed deployment") + summary := MockSucessSummaryResult(object, "test-hash") + summary.Summary.SuccessCount = 0 + summary.Summary.AllAssignedDeployed = false + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + It("should not queue a reconcile job because the reconcile session is done", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + Expect(reconcileResult.Requeue).To(BeFalse()) + Expect(reconcileResult.RequeueAfter).To(BeZero()) + }) + }) + }) + + When("reconcile policy state is inactive", func() { + BeforeEach(func(ctx context.Context) { + By("updating the object with a valid reconcile policy: state inactive, interval 10m (interval will be ignored)") + object.Spec.ReconciliationPolicy = &k8smodel.ReconciliationPolicySpec{State: "inactive", Interval: ToPointer("10m")} + err := kubeClient.Update(ctx, object) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("deployment is not queued to the api due to non-terminal error", func() { + BeforeEach(func() { + By("mocking the summary response with not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking the queue deployment response with a non-terminal error") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test error")) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should re-queue a reconcile job immediately", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + It("should have a status of reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + + Context("deployment is not queued to the api due to terminal error", func() { + + BeforeEach(func() { + By("mocking the summary response with not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking the queue deployment response with a terminal error") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(TerminalError) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + It("should not queue a reconcile job because the reconcile session is done", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + Expect(reconcileResult.Requeue).To(BeFalse()) + Expect(reconcileResult.RequeueAfter).To(BeZero()) + }) + }) + + Context("deployment job queued successfully", func() { + BeforeEach(func() { + By("mocking the summary response with not found error") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("mocking the queue deployment response with a successful deployment") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should queue a reconcile job to poll for status", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestPollInterval)) + }) + + It("should have a status of reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + + Context("deployment to api is completed successful", func() { + BeforeEach(func() { + By("mocking the summary response with a successful deployment") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(object, "test-hash"), nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of succeeded", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Succeeded")) + }) + + It("should not queue a reconcile job because the reconcile session is done", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + Expect(reconcileResult.Requeue).To(BeFalse()) + Expect(reconcileResult.RequeueAfter).To(BeZero()) + }) + }) + Context("deployment to api is completed with failure", func() { + BeforeEach(func() { + By("mocking the summary response with a failed deployment") + summary := MockSucessSummaryResult(object, "test-hash") + summary.Summary.SuccessCount = 0 + summary.Summary.AllAssignedDeployed = false + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should make the expected api calls", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should have a status of failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + It("should not queue a reconcile job because the reconcile session is done", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + Expect(reconcileResult.Requeue).To(BeFalse()) + Expect(reconcileResult.RequeueAfter).To(BeZero()) + }) + }) + }) + }) + + Context("object has stil not finished reconciling and has timed out", func() { + BeforeEach(func(ctx context.Context) { + By("mocking a summary response with in progress deployment") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockInProgressSummaryResult(object, "test-hash"), nil) + }) + + JustBeforeEach(func(ctx context.Context) { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + + By("updating the object operation start time to be in the past") + object.GetAnnotations()[targetOperationStartTimeKey] = time.Now().Add(-time.Hour).Format(time.RFC3339) + Expect(kubeClient.Update(ctx, object)).NotTo(HaveOccurred()) + }) + + JustBeforeEach(func(ctx context.Context) { + By("calling the reconciler") + _, reconcileResult, reconcileError = reconciler.AttemptUpdate(ctx, object, logr.Discard(), targetOperationStartTimeKey) + }) + + It("should not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + It("should queue a reconcile job after reconcile interval", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("10ms").Of(TestReconcileInterval)) + }) + + It("should have a status of failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + }) +}) diff --git a/k8s/reconcilers/reconciler.go b/k8s/reconcilers/reconciler.go new file mode 100644 index 000000000..ca72497fa --- /dev/null +++ b/k8s/reconcilers/reconciler.go @@ -0,0 +1,30 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package reconcilers + +import ( + "context" + apiV1 "gopls-workspace/apis/model/v1" + "gopls-workspace/controllers/metrics" + + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type ( + Reconciler interface { + AttemptUpdate(ctx context.Context, object Reconcilable, logger logr.Logger, operationStartTimeKey string) (metrics.OperationStatus, reconcile.Result, error) + AttemptRemove(ctx context.Context, object Reconcilable, logger logr.Logger, operationStartTimeKey string) (metrics.OperationStatus, reconcile.Result, error) + } + Reconcilable interface { + client.Object + GetStatus() apiV1.DeployableStatus + SetStatus(apiV1.DeployableStatus) + GetReconciliationPolicy() *apiV1.ReconciliationPolicySpec + } +) diff --git a/k8s/reconcilers/suite_test.go b/k8s/reconcilers/suite_test.go new file mode 100644 index 000000000..9583567f1 --- /dev/null +++ b/k8s/reconcilers/suite_test.go @@ -0,0 +1,21 @@ +package reconcilers_test + +import ( + "testing" + + "gopls-workspace/constants" + internalTesting "gopls-workspace/testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + targetOperationStartTimeKey = "target.fabric." + constants.OperationStartTimeKeyPostfix +) + +func TestSuiteReconcilers(t *testing.T) { + + RegisterFailHandler(Fail) + internalTesting.RunGinkgoSpecs(t, "Reconcilers Suite") +} diff --git a/k8s/reconcilers/update_test.go b/k8s/reconcilers/update_test.go new file mode 100644 index 000000000..2aa74ca82 --- /dev/null +++ b/k8s/reconcilers/update_test.go @@ -0,0 +1,375 @@ +package reconcilers_test + +import ( + "context" + "errors" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/go-logr/logr" + "github.com/stretchr/testify/mock" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + solutionv1 "gopls-workspace/apis/solution/v1" + "gopls-workspace/reconcilers" + + . "gopls-workspace/testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Creating a reconciler", func() { + When("no options are provided", func() { + It("should return an error", func() { + _, err := reconcilers.NewDeploymentReconciler() + Expect(err).To(HaveOccurred()) + }) + }) + + When("only finalizer name is provided", func() { + It("should return an error", func() { + _, err := reconcilers.NewDeploymentReconciler( + reconcilers.WithFinalizerName("test-finalizer"), + ) + Expect(err).To(HaveOccurred()) + }) + }) + + When("only finalizer and kube client are provided", func() { + It("should return an error", func() { + _, err := reconcilers.NewDeploymentReconciler( + reconcilers.WithFinalizerName("test-finalizer"), + reconcilers.WithClient(CreateFakeKubeClientForFabricGroup()), + ) + Expect(err).To(HaveOccurred()) + }) + }) + + When("only finalizer, kube client and api client are provided", func() { + It("should return an error", func() { + _, err := reconcilers.NewDeploymentReconciler( + reconcilers.WithFinalizerName("test-finalizer"), + reconcilers.WithClient(CreateFakeKubeClientForFabricGroup()), + reconcilers.WithApiClient(&MockApiClient{}), + ) + Expect(err).To(HaveOccurred()) + }) + }) + + When("all required options are provided", func() { + It("should not return an error", func() { + _, err := reconcilers.NewDeploymentReconciler( + reconcilers.WithFinalizerName("test-finalizer"), + reconcilers.WithClient(CreateFakeKubeClientForFabricGroup()), + reconcilers.WithApiClient(&MockApiClient{}), + reconcilers.WithDeploymentBuilder(CreateSimpleDeploymentBuilder()), + ) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + When("all required and default options are provided", func() { + It("should not return an error", func() { + reconciler, err := reconcilers.NewDeploymentReconciler(append( + DefaultTestReconcilerOptions(), + reconcilers.WithClient(CreateFakeKubeClientForFabricGroup()), + reconcilers.WithApiClient(&MockApiClient{}), + reconcilers.WithDeploymentErrorBuilder(func(*model.SummaryResult, error, *apimodel.ErrorType) {}), + reconcilers.WithDeploymentKeyResolver(func(obj reconcilers.Reconcilable) string { return obj.GetName() }), + )...) + Expect(err).NotTo(HaveOccurred()) + Expect(reconciler).NotTo(BeNil()) + }) + }) +}) + +var _ = Describe("Calling 'AttemptUpdate' on object", func() { + Context("all required and default options are provided and default objects exist in kube api", func() { + var reconciler *reconcilers.DeploymentReconciler + var apiClient *MockApiClient + var kubeClient client.Client + var object *solutionv1.Instance + var reconcileResult reconcile.Result + var reconcileError error + + BeforeEach(func() { + By("setting up the reconciler") + apiClient = &MockApiClient{} + kubeClient = CreateFakeKubeClientForSolutionGroup( + BuildDefaultInstance(), + ) + var err error + reconciler, err = reconcilers.NewDeploymentReconciler(append( + DefaultTestReconcilerOptions(), + reconcilers.WithApiClient(apiClient), + reconcilers.WithClient(kubeClient))..., + ) + Expect(err).NotTo(HaveOccurred()) + }) + + BeforeEach(func(ctx context.Context) { + By("fetching the latest resources from kube api") + object = &solutionv1.Instance{} + err := kubeClient.Get(ctx, DefaultInstanceNamespacedName, object) + Expect(err).NotTo(HaveOccurred()) + }) + + JustBeforeEach(func(ctx context.Context) { + By("calling the reconciler") + _, reconcileResult, reconcileError = reconciler.AttemptUpdate(ctx, object, logr.Discard(), targetOperationStartTimeKey) + }) + + When("object is successfully deployed", func() { + BeforeEach(func(ctx context.Context) { + By("setting up the api client with a successful response") + + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(object, "test-hash"), nil) + }) + + It("should add a finalizer to the object", func() { + Expect(object.GetFinalizers()).To(ContainElement("test-finalizer")) + }) + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + It("should have status Succeeded", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + Expect(object.Status.ProvisioningStatus.Status).To(Equal("Succeeded")) + }) + It("should requue after some time", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1ms").Of(TestReconcileInterval)) + }) + }) + + When("object has not been deployed", func() { + Context("api returns not found when queried for summary", func() { + BeforeEach(func(ctx context.Context) { + By("setting up the api client with an undeployed response") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + + By("setting up the api client with a successful deployment queued response") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + It("should have status Reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(Equal("Reconciling")) + }) + + It("should requue after some time", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1ms").Of(TestPollInterval)) + }) + }) + + Context("api returns pending when queried for summary", func() { + BeforeEach(func(ctx context.Context) { + By("setting up the api client with an pending response") + summary := MockSucessSummaryResult(object, "test-hash") + summary.State = model.SummaryStatePending + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + Expect(reconcileError).NotTo(HaveOccurred()) + }) + + It("should requue after some time", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1ms").Of(TestPollInterval)) + }) + }) + + Context("api returns an error when queried for summary", func() { + BeforeEach(func(ctx context.Context) { + By("setting up the api client with an error response") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("test error")) + }) + + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should requeue because of error", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("test error")) // TODO: this is not ideal. Error should be in a separate field + Expect(reconcileError).To(HaveOccurred()) + }) + + It("should have status Reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + + Context("a terminal error occurs when trying to queue deployment job", func() { + BeforeEach(func(ctx context.Context) { + By("setting up the api client with a successful deployment queued response") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(TerminalError) + }) + + It("should not queue further reconcile jobs", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should have a provisioning status of failed", func() {}) + }) + + Context("a non-terminal error occurs when trying to queue deployment job", func() { + BeforeEach(func(ctx context.Context) { + By("setting up the api client with a successful deployment queued response") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(nil, NotFoundError) + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test error")) + }) + It("should queue further reconcile jobs due to error", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + It("should have a provisioning status of reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("test error")) // TODO: this is not ideal. Error should be in a separate field + }) + }) + }) + + When("object is reconciling to poll for deployment status", func() { + Context("api returns a summary indicating deployment is in progress", func() { + BeforeEach(func(ctx context.Context) { + By("setting up the api client with a successful deployment queued response") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockInProgressSummaryResult(object, "test-hash"), nil) + }) + + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should have status Reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(Equal("Reconciling")) + }) + It("should requue after some time", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1ms").Of(TestPollInterval)) + }) + It("should updpate the compoent status with progress", func() { + Expect(object.Status.Properties["targets.default-target.comp1"]).To(ContainSubstring("updated")) + Expect(object.Status.Properties["targets.default-target.comp2"]).To(ContainSubstring("pending")) + }) + }) + + When("api returns a summary for a different version of the object", func() { + BeforeEach(func() { + By("setting up the api client with a summary response for a different version of the object") + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(MockSucessSummaryResult(object, "another-hash"), nil) + }) + Context("successfully queues a deployment job to api", func() { + BeforeEach(func(ctx context.Context) { + By("allowing a succesful queued deployment response") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should have status Reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + It("should requue after some time", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1ms").Of(TestPollInterval)) + }) + }) + + Context("fails to queue a deployment job due to a non-terminal error", func() { + BeforeEach(func(ctx context.Context) { + By("mocking a non-terminal error when queuing a deployment job") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test error")) + }) + + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should requeue due to error", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + It("should have status Reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + }) + + When("api returns a summary that's older than the reconcile interval", func() { + BeforeEach(func() { + By("mocking a summary that's older than the reconcile interval") + summary := MockSucessSummaryResult(object, "test-hash") + summary.Time = summary.Time.Add(-20 * TestReconcileInterval) + apiClient.On("GetSummary", mock.Anything, mock.Anything, mock.Anything).Return(summary, nil) + }) + + Context("successfully queues a deployment job to api", func() { + BeforeEach(func(ctx context.Context) { + By("allowing a succesful queued deployment response") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + }) + + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should not return an error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should have status Reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + It("should requue after some time", func() { + Expect(reconcileResult.RequeueAfter).To(BeWithin("1ms").Of(TestPollInterval)) + }) + }) + + Context("fails to queue a deployment job due to a non-terminal error", func() { + BeforeEach(func(ctx context.Context) { + By("mocking a non-terminal error when queuing a deployment job") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test error")) + }) + + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should requeue due to error", func() { + Expect(reconcileError).To(HaveOccurred()) + }) + It("should have status Reconciling", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Reconciling")) + }) + }) + + Context("fails to queue a deployment job due to a terminal error", func() { + BeforeEach(func(ctx context.Context) { + By("mocking a terminal error when queuing a deployment job") + apiClient.On("QueueDeploymentJob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(TerminalError) + }) + + It("should call api client with correct parameters", func() { + apiClient.AssertExpectations(GinkgoT()) + }) + + It("should not requeue due to error", func() { + Expect(reconcileError).NotTo(HaveOccurred()) + }) + It("should have status Failed", func() { + Expect(object.Status.ProvisioningStatus.Status).To(ContainSubstring("Failed")) + }) + }) + }) + }) + }) +}) diff --git a/k8s/testing/mocks.go b/k8s/testing/mocks.go new file mode 100644 index 000000000..aa99789f5 --- /dev/null +++ b/k8s/testing/mocks.go @@ -0,0 +1,383 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package testing + +import ( + "context" + "errors" + "fmt" + ai_v1 "gopls-workspace/apis/ai/v1" + fabric_v1 "gopls-workspace/apis/fabric/v1" + federation_v1 "gopls-workspace/apis/federation/v1" + k8smodel "gopls-workspace/apis/model/v1" + solution_v1 "gopls-workspace/apis/solution/v1" + "gopls-workspace/reconcilers" + "gopls-workspace/utils" + "strconv" + "time" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + gomegaTypes "github.com/onsi/gomega/types" + "github.com/stretchr/testify/mock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +type ( + MockApiClient struct { + mock.Mock + } + + MockDelayer struct { + mock.Mock + } + + TimeMatcher struct { + wiggleroom time.Duration + duration time.Duration + } +) + +const ( + TestFinalizer = "test-finalizer" + TestPollInterval = 1 * time.Second + TestReconcileInterval = 10 * TestPollInterval + TestReconcileTimout = 20 * TestPollInterval + TestDeleteSyncDelay = 5 * time.Second + TestNamespace = "default" + TestScope = "symphony-test-scope" +) + +var ( + _ utils.ApiClient = &MockApiClient{} + _ gomegaTypes.GomegaMatcher = &TimeMatcher{} +) + +var ( + DefaultTargetNamepsacedName = types.NamespacedName{Name: "test-target", Namespace: TestNamespace} + DefaultInstanceNamespacedName = types.NamespacedName{Name: "test-instance", Namespace: TestNamespace} + DefaultSolutionNamespacedName = types.NamespacedName{Name: "test-solution", Namespace: TestNamespace} + + TerminalError = v1alpha2.NewCOAError(errors.New(""), "timed out", v1alpha2.TimedOut) + NotFoundError = v1alpha2.NewCOAError(errors.New(""), "not found", v1alpha2.NotFound) +) + +func (m *MockDelayer) Sleep(duration time.Duration) { + m.Called(duration) +} + +func CreateFakeKubeClientForSolutionAndFabricGroup(objects ...client.Object) client.Client { + scheme := runtime.NewScheme() + if objects == nil { + objects = []client.Object{ + BuildDefaultInstance(), + BuildDefaultSolution(), + BuildDefaultTarget(), + } + } + + _ = solution_v1.AddToScheme(scheme) + _ = fabric_v1.AddToScheme(scheme) + return fake.NewClientBuilder(). + WithObjects(objects...). + WithScheme(scheme). + Build() +} + +func CreateFakeKubeClientForSolutionGroup(objects ...client.Object) client.Client { + scheme := runtime.NewScheme() + if objects == nil { + objects = []client.Object{ + BuildDefaultInstance(), + BuildDefaultSolution(), + } + } + + _ = solution_v1.AddToScheme(scheme) + return fake.NewClientBuilder(). + WithObjects(objects...). + WithScheme(scheme). + Build() +} + +func CreateFakeKubeClientForFabricGroup(objects ...client.Object) client.Client { + scheme := runtime.NewScheme() + if objects == nil { + objects = []client.Object{ + BuildDefaultTarget(), + } + } + + _ = fabric_v1.AddToScheme(scheme) + return fake.NewClientBuilder(). + WithObjects(objects...). + WithScheme(scheme). + Build() +} + +func CreateFakeKubeClientForFederationGroup(objects ...client.Object) client.Client { + scheme := runtime.NewScheme() + if objects == nil { + objects = []client.Object{} + } + + _ = federation_v1.AddToScheme(scheme) + return fake.NewClientBuilder(). + WithObjects(objects...). + WithScheme(scheme). + Build() +} + +func CreateFakeKubeClientForAIGroup(objects ...client.Object) client.Client { + scheme := runtime.NewScheme() + if objects == nil { + objects = []client.Object{} + } + + _ = ai_v1.AddToScheme(scheme) + return fake.NewClientBuilder(). + WithObjects(objects...). + WithScheme(scheme). + Build() +} + +func CreateFakeKubeClientForWorkflowGroup(objects ...client.Object) client.Client { + scheme := runtime.NewScheme() + if objects == nil { + objects = []client.Object{} + } + + _ = ai_v1.AddToScheme(scheme) + return fake.NewClientBuilder(). + WithObjects(objects...). + WithScheme(scheme). + Build() +} + +func MockSucessSummaryResult(obj reconcilers.Reconcilable, hash string) *model.SummaryResult { + return &model.SummaryResult{ + Summary: model.SummarySpec{ + TargetCount: 1, + SuccessCount: 1, + AllAssignedDeployed: true, + }, + Time: time.Now(), + State: model.SummaryStateDone, + Generation: strconv.Itoa(int(obj.GetGeneration())), + DeploymentHash: hash, + } +} + +func MockFailureSummaryResult(obj reconcilers.Reconcilable, hash string) *model.SummaryResult { + return &model.SummaryResult{ + Summary: model.SummarySpec{ + TargetCount: 1, + SuccessCount: 0, + AllAssignedDeployed: false, + TargetResults: map[string]model.TargetResultSpec{ + "default-target": { + Status: "ErrorCode", + ComponentResults: map[string]model.ComponentResultSpec{ + "comp1": {Status: v1alpha2.UpdateFailed, Message: "failed"}, + "comp2": {Status: v1alpha2.Accepted, Message: "untoched"}, + }, + }, + }, + }, + Time: time.Now(), + State: model.SummaryStateDone, + Generation: strconv.Itoa(int(obj.GetGeneration())), + DeploymentHash: hash, + } +} + +func MockInProgressSummaryResult(obj reconcilers.Reconcilable, hash string) *model.SummaryResult { + return &model.SummaryResult{ + Summary: model.SummarySpec{ + TargetCount: 1, + SuccessCount: 0, + AllAssignedDeployed: false, + TargetResults: map[string]model.TargetResultSpec{ + "default-target": { + Status: "pending", + ComponentResults: map[string]model.ComponentResultSpec{ + "comp1": {Status: v1alpha2.Updated, Message: "updated"}, + "comp2": {Status: v1alpha2.Accepted, Message: "pending"}, + }, + }, + }, + }, + Time: time.Now(), + State: model.SummaryStateRunning, + Generation: strconv.Itoa(int(obj.GetGeneration())), + DeploymentHash: hash, + } +} + +func MockInProgressDeleteSummaryResult(obj reconcilers.Reconcilable, hash string) *model.SummaryResult { + return &model.SummaryResult{ + Summary: model.SummarySpec{ + TargetCount: 1, + SuccessCount: 0, + AllAssignedDeployed: false, + TargetResults: map[string]model.TargetResultSpec{ + "default-target": { + Status: "pending", + ComponentResults: map[string]model.ComponentResultSpec{ + "comp1": {Status: v1alpha2.Updated, Message: "deleted"}, + "comp2": {Status: v1alpha2.Accepted, Message: "pending"}, + }, + }, + }, + IsRemoval: true, + }, + Time: time.Now(), + State: model.SummaryStateRunning, + Generation: strconv.Itoa(int(obj.GetGeneration())), + DeploymentHash: hash, + } +} + +// GetSummary implements ApiClient. +func (c *MockApiClient) GetSummary(ctx context.Context, id string, namespace string) (*model.SummaryResult, error) { + args := c.Called(ctx, id, namespace) + summary := args.Get(0) + if summary == nil { + return nil, args.Error(1) + } + return summary.(*model.SummaryResult), args.Error(1) +} + +// QueueDeploymentJob implements utils.ApiClient. +func (c *MockApiClient) QueueDeploymentJob(ctx context.Context, namespace string, isDelete bool, deployment model.DeploymentSpec) error { + args := c.Called(ctx, namespace, isDelete, deployment) + return args.Error(0) +} + +// QueueJob implements ApiClient. +// Deprecated and not used. +func (c *MockApiClient) QueueJob(ctx context.Context, id string, scope string, isDelete bool, isTarget bool) error { + panic("implement me") +} + +func CreateSimpleDeploymentBuilder() func(ctx context.Context, object reconcilers.Reconcilable) (*model.DeploymentSpec, error) { + return func(ctx context.Context, object reconcilers.Reconcilable) (*model.DeploymentSpec, error) { + return &model.DeploymentSpec{ + Hash: "test-hash", + }, nil + } +} + +func createDeploymentBuilder(dr utils.DeploymentResources) func(ctx context.Context, object reconcilers.Reconcilable) (*model.DeploymentSpec, error) { + return func(ctx context.Context, object reconcilers.Reconcilable) (*model.DeploymentSpec, error) { + deployment, err := utils.CreateSymphonyDeployment( + ctx, + dr.Instance, + dr.Solution, + dr.TargetCandidates, + TestNamespace, + ) + return &deployment, err + } +} + +func BuildDefaultInstance() *solution_v1.Instance { + return &solution_v1.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: DefaultInstanceNamespacedName.Name, + Namespace: DefaultInstanceNamespacedName.Namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Instance", + APIVersion: solution_v1.GroupVersion.String(), + }, + Spec: k8smodel.InstanceSpec{ + Target: model.TargetSelector{ + Name: DefaultTargetNamepsacedName.Name, + }, + Solution: DefaultSolutionNamespacedName.Name, + }, + } +} + +func BuildDefaultTarget() *fabric_v1.Target { + return &fabric_v1.Target{ + ObjectMeta: metav1.ObjectMeta{ + Name: DefaultTargetNamepsacedName.Name, + Namespace: DefaultTargetNamepsacedName.Namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Target", + APIVersion: fabric_v1.GroupVersion.String(), + }, + Spec: k8smodel.TargetSpec{ + Scope: TestScope, + }, + } +} + +func BuildDefaultSolution() *solution_v1.Solution { + return &solution_v1.Solution{ + ObjectMeta: metav1.ObjectMeta{ + Name: DefaultSolutionNamespacedName.Name, + Namespace: DefaultSolutionNamespacedName.Namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Solution", + APIVersion: solution_v1.GroupVersion.String(), + }, + Spec: k8smodel.SolutionSpec{}, + } +} + +func DefaultTestReconcilerOptions() []reconcilers.DeploymentReconcilerOptions { + return []reconcilers.DeploymentReconcilerOptions{ + reconcilers.WithDeleteTimeOut(TestReconcileTimout), + reconcilers.WithPollInterval(TestPollInterval), + reconcilers.WithReconciliationInterval(TestReconcileInterval), + reconcilers.WithFinalizerName(TestFinalizer), + reconcilers.WithDeploymentBuilder(CreateSimpleDeploymentBuilder()), + } +} + +func BeWithin(durationString string) *TimeMatcher { + duration, err := time.ParseDuration(durationString) + if err != nil { + panic(err) + } + return &TimeMatcher{ + wiggleroom: duration, + } +} + +func (m *TimeMatcher) Of(expected time.Duration) *TimeMatcher { + m.duration = expected + return m +} + +func (m *TimeMatcher) Match(actual interface{}) (success bool, err error) { + duration, ok := actual.(time.Duration) + if !ok { + return false, nil + } + return duration >= m.duration-m.wiggleroom && duration <= m.duration+m.wiggleroom, nil +} + +func (m *TimeMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%s\nto be within\n\t%s\nof\n\t%s", actual, m.wiggleroom, m.duration) +} + +func (m *TimeMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%#v\nnot to be within\n\t%#v\nof\n\t%#v", actual, m.wiggleroom, m.duration) +} + +func ToPointer[T any](v T) *T { return &v } diff --git a/k8s/utils/helper.go b/k8s/utils/helper.go index 26732df87..cc8b3af2f 100644 --- a/k8s/utils/helper.go +++ b/k8s/utils/helper.go @@ -7,10 +7,50 @@ package utils import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" "regexp" + "sort" + + "gopls-workspace/constants" + + "sigs.k8s.io/controller-runtime/pkg/client" ) +var targetKeyRegex = regexp.MustCompile(`^targets\.[^.]+\.[^.]+`) + func IsComponentKey(key string) bool { - regex := regexp.MustCompile(`^targets\.[^.]+\.[^.]+`) - return regex.MatchString(key) + return targetKeyRegex.MatchString(key) +} + +func HashObjects(deploymentResources DeploymentResources) string { + hasher := md5.New() + + // Sort the targets by name + sort.Slice(deploymentResources.TargetCandidates, func(i, j int) bool { + return deploymentResources.TargetCandidates[i].GetName() < deploymentResources.TargetCandidates[j].GetName() + }) + + // Add the solution and instance to the hasher + writeObjectHash(hasher, &deploymentResources.Solution) + writeObjectHash(hasher, &deploymentResources.Instance) + + // Add the sorted targets to the hasher + for _, target := range deploymentResources.TargetCandidates { + writeObjectHash(hasher, &target) + } + + // Get the final hash result + return hex.EncodeToString(hasher.Sum(nil)) +} + +func writeObjectHash(writer io.Writer, object client.Object) { + fmt.Fprintf(writer, "<%s:%s:%s:%d>", + object.GetName(), + object.GetObjectKind().GroupVersionKind().Kind, + object.GetAnnotations()[constants.AzureOperationIdKey], + object.GetGeneration(), + ) } diff --git a/k8s/utils/model/provisioning-states.go b/k8s/utils/model/provisioning-states.go new file mode 100644 index 000000000..088a6c43c --- /dev/null +++ b/k8s/utils/model/provisioning-states.go @@ -0,0 +1,34 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package model + +import "sigs.k8s.io/controller-runtime/pkg/client" + +// ARM has a strict requirement on what the terminal states need to be for the provisioning of resources through the LRO contract (Succeeded, Failed, Cancelled) +// The documentation that talks about this can be found here: https://armwiki.azurewebsites.net/rpaas/async.html#provisioningstate-property +// The below exported members capture these states. The first three are the terminal states required by ARM and the +// fourth is a non-terminal state we use to indicate that the resource is being reconciled. +type ProvisioningStatus string + +const ( + ProvisioningStatusSucceeded ProvisioningStatus = "Succeeded" + ProvisioningStatusFailed ProvisioningStatus = "Failed" + ProvisioningStatusCancelled ProvisioningStatus = "Cancelled" + ProvisioningStatusReconciling ProvisioningStatus = "Reconciling" + ProvisioningStatusDeleting ProvisioningStatus = "Deleting" +) + +func IsTerminalState(status string) bool { + return status == string(ProvisioningStatusSucceeded) || status == string(ProvisioningStatusFailed) +} + +func GetNonTerminalStatus(object client.Object) ProvisioningStatus { + if object.GetDeletionTimestamp() != nil { + return ProvisioningStatusDeleting + } + return ProvisioningStatusReconciling +} diff --git a/k8s/utils/models/provisioning-states.go b/k8s/utils/models/provisioning-states.go deleted file mode 100644 index 4674e1ac2..000000000 --- a/k8s/utils/models/provisioning-states.go +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * SPDX-License-Identifier: MIT - */ - -package provisioningstates - -// ARM has a strict requirement on what the terminal states need to be for the provisioning of resources through the LRO contract (Succeeded, Failed, Cancelled) -// The documentation that talks about this can be found here: https://armwiki.azurewebsites.net/rpaas/async.html#provisioningstate-property -// The below exported members capture these states. The first three are the terminal states required by ARM and the -// fourth is a non-terminal state we use to indicate that the resource is being reconciled. -const ( - Succeeded = "Succeeded" - Failed = "Failed" - Cancelled = "Cancelled" - Reconciling = "Reconciling" -) diff --git a/k8s/utils/symphony-api.go b/k8s/utils/symphony-api.go new file mode 100644 index 000000000..2ef63a2c3 --- /dev/null +++ b/k8s/utils/symphony-api.go @@ -0,0 +1,241 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package utils + +import ( + "context" + "encoding/json" + "gopls-workspace/constants" + "os" + "regexp" + "strconv" + "strings" + + apimodel "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + api_utils "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" + + fabric_v1 "gopls-workspace/apis/fabric/v1" + k8smodel "gopls-workspace/apis/model/v1" + solution_v1 "gopls-workspace/apis/solution/v1" +) + +var ( + SymphonyAPIAddressBase = os.Getenv(constants.SymphonyAPIUrlEnvName) +) + +type ( + ApiClient interface { + api_utils.SummaryGetter + api_utils.Dispatcher + } + + DeploymentResources struct { + Instance solution_v1.Instance + Solution solution_v1.Solution + TargetList fabric_v1.TargetList + TargetCandidates []fabric_v1.Target + } +) + +func K8SSidecarSpecToAPISidecarSpec(sidecar k8smodel.SidecarSpec) (apimodel.SidecarSpec, error) { + sidecarSpec := apimodel.SidecarSpec{} + data, _ := json.Marshal(sidecar) + var err error + err = json.Unmarshal(data, &sidecarSpec) + if err != nil { + return apimodel.SidecarSpec{}, err + } + sidecarSpec.Properties = make(map[string]interface{}) + err = json.Unmarshal(sidecar.Properties.Raw, &sidecarSpec.Properties) + return sidecarSpec, err +} + +func K8SComponentSpecToAPIComponentSpec(component k8smodel.ComponentSpec) (apimodel.ComponentSpec, error) { + componentSpec := apimodel.ComponentSpec{} + data, _ := json.Marshal(component) + var err error + err = json.Unmarshal(data, &componentSpec) + if err != nil { + return apimodel.ComponentSpec{}, err + } + componentSpec.Properties = make(map[string]interface{}) + err = json.Unmarshal(component.Properties.Raw, &componentSpec.Properties) + return componentSpec, err +} + +func K8STargetToAPITargetState(target fabric_v1.Target) (apimodel.TargetState, error) { + ret := apimodel.TargetState{ + ObjectMeta: apimodel.ObjectMeta{ + Name: target.ObjectMeta.Name, + Namespace: target.ObjectMeta.Namespace, + Labels: target.ObjectMeta.Labels, + Annotations: target.ObjectMeta.Annotations, + }, + Spec: &apimodel.TargetSpec{ + DisplayName: target.Spec.DisplayName, + Metadata: target.Spec.Metadata, + Scope: target.Spec.Scope, + Properties: target.Spec.Properties, + Constraints: target.Spec.Constraints, + ForceRedeploy: target.Spec.ForceRedeploy, + Topologies: target.Spec.Topologies, + Generation: target.Spec.Generation, + }, + } + + var err error + ret.Spec.Components = make([]apimodel.ComponentSpec, len(target.Spec.Components)) + for i, c := range target.Spec.Components { + ret.Spec.Components[i], err = K8SComponentSpecToAPIComponentSpec(c) + if err != nil { + return apimodel.TargetState{}, err + } + } + + return ret, nil +} + +func K8SInstanceToAPIInstanceState(instance solution_v1.Instance) (apimodel.InstanceState, error) { + ret := apimodel.InstanceState{ + ObjectMeta: apimodel.ObjectMeta{ + Name: instance.ObjectMeta.Name, + Namespace: instance.ObjectMeta.Namespace, + Labels: instance.ObjectMeta.Labels, + Annotations: instance.ObjectMeta.Annotations, + }, + Spec: &apimodel.InstanceSpec{ + Scope: instance.Spec.Scope, + Name: instance.Spec.Name, + DisplayName: instance.Spec.DisplayName, + Solution: instance.Spec.Solution, + Target: instance.Spec.Target, + Parameters: instance.Spec.Parameters, + Metadata: instance.Spec.Metadata, + Generation: instance.Spec.Generation, + Topologies: instance.Spec.Topologies, + Pipelines: instance.Spec.Pipelines, + }, + } + + return ret, nil +} + +func K8SSolutionToAPISolutionState(solution solution_v1.Solution) (apimodel.SolutionState, error) { + ret := apimodel.SolutionState{ + ObjectMeta: apimodel.ObjectMeta{ + Name: solution.ObjectMeta.Name, + Namespace: solution.ObjectMeta.Namespace, + Labels: solution.ObjectMeta.Labels, + Annotations: solution.ObjectMeta.Annotations, + }, + Spec: &apimodel.SolutionSpec{ + DisplayName: solution.Spec.DisplayName, + Metadata: solution.Spec.Metadata, + }, + } + + var err error + ret.Spec.Components = make([]apimodel.ComponentSpec, len(solution.Spec.Components)) + for i, t := range solution.Spec.Components { + ret.Spec.Components[i], err = K8SComponentSpecToAPIComponentSpec(t) + if err != nil { + return apimodel.SolutionState{}, err + } + } + return ret, nil + +} + +func matchString(src string, target string) bool { + if strings.Contains(src, "*") || strings.Contains(src, "%") { + p := strings.ReplaceAll(src, "*", ".*") + p = strings.ReplaceAll(p, "%", ".") + re := regexp.MustCompile(p) + return re.MatchString(target) + } else { + return src == target + } +} + +func MatchTargets(instance solution_v1.Instance, targets fabric_v1.TargetList) []fabric_v1.Target { + ret := make(map[string]fabric_v1.Target) + if instance.Spec.Target.Name != "" { + for _, t := range targets.Items { + + if matchString(instance.Spec.Target.Name, t.ObjectMeta.Name) { + ret[t.ObjectMeta.Name] = t + } + } + } + if len(instance.Spec.Target.Selector) > 0 { + for _, t := range targets.Items { + fullMatch := true + for k, v := range instance.Spec.Target.Selector { + if tv, ok := t.Spec.Properties[k]; !ok || !matchString(v, tv) { + fullMatch = false + } + } + if fullMatch { + ret[t.ObjectMeta.Name] = t + } + } + } + slice := make([]fabric_v1.Target, 0, len(ret)) + for _, v := range ret { + slice = append(slice, v) + } + return slice +} + +func CreateSymphonyDeploymentFromTarget(target fabric_v1.Target, namespace string) (apimodel.DeploymentSpec, error) { + targetState, err := K8STargetToAPITargetState(target) + if err != nil { + return apimodel.DeploymentSpec{}, err + } + + var ret apimodel.DeploymentSpec + ret, err = api_utils.CreateSymphonyDeploymentFromTarget(targetState, namespace) + ret.Hash = HashObjects(DeploymentResources{ + TargetCandidates: []fabric_v1.Target{target}, + }) + + ret.Generation = strconv.Itoa(int(target.ObjectMeta.Generation)) + + return ret, err +} + +func CreateSymphonyDeployment(ctx context.Context, instance solution_v1.Instance, solution solution_v1.Solution, targets []fabric_v1.Target, objectNamespace string) (apimodel.DeploymentSpec, error) { + instanceState, err := K8SInstanceToAPIInstanceState(instance) + if err != nil { + return apimodel.DeploymentSpec{}, err + } + + solutionState, err := K8SSolutionToAPISolutionState(solution) + if err != nil { + return apimodel.DeploymentSpec{}, err + } + + targetStates := make([]apimodel.TargetState, len(targets)) + for i, t := range targets { + targetStates[i], err = K8STargetToAPITargetState(t) + if err != nil { + return apimodel.DeploymentSpec{}, err + } + } + + var ret apimodel.DeploymentSpec + ret, err = api_utils.CreateSymphonyDeployment(instanceState, solutionState, targetStates, nil, objectNamespace) + ret.Hash = HashObjects(DeploymentResources{ + Instance: instance, + Solution: solution, + TargetCandidates: targets, + }) + + ret.Generation = strconv.Itoa(int(instance.ObjectMeta.Generation)) + + return ret, err +} diff --git a/k8s/utils/symphony-api_test.go b/k8s/utils/symphony-api_test.go new file mode 100644 index 000000000..b68e1671a --- /dev/null +++ b/k8s/utils/symphony-api_test.go @@ -0,0 +1,80 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package utils + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/yaml" + + solution_v1 "gopls-workspace/apis/solution/v1" +) + +func TestK8SSolutionToAPISolutionState(t *testing.T) { + solutionYaml := `apiVersion: solution.symphony/v1 +kind: Solution +metadata: + name: sample-staged-solution +spec: + components: + - name: staged-component + properties: + foo: "bar" + bar: + baz: "qux" + sidecars: + - name: sidecar1 + type: container + properties: + container.image: "symphony/sidecar" + env.foo: "bar" + nestedEnv: + baz: "qux" +` + solution := &solution_v1.Solution{} + err := yaml.Unmarshal([]byte(solutionYaml), solution) + assert.NoError(t, err) + + expectedProperties := map[string]interface{}{ + "foo": "bar", + "bar": map[string]interface{}{ + "baz": "qux", + }, + } + actualProperties := map[string]interface{}{} + err = json.Unmarshal(solution.Spec.Components[0].Properties.Raw, &actualProperties) + assert.NoError(t, err) + + assert.Equal(t, expectedProperties, actualProperties) + + expectedSidecarProperties := map[string]interface{}{ + "container.image": "symphony/sidecar", + "env.foo": "bar", + "nestedEnv": map[string]interface{}{ + "baz": "qux", + }, + } + actualSidecarProperties := map[string]interface{}{} + err = json.Unmarshal(solution.Spec.Components[0].Sidecars[0].Properties.Raw, &actualSidecarProperties) + assert.NoError(t, err) + + assert.Equal(t, expectedSidecarProperties, actualSidecarProperties) + + apiSolutionState, err := K8SSolutionToAPISolutionState(*solution) + + assert.NoError(t, err) + assert.Equal(t, solution.Name, apiSolutionState.ObjectMeta.Name) + assert.Equal(t, solution.Spec.Components[0].Name, apiSolutionState.Spec.Components[0].Name) + assert.Equal(t, solution.Spec.Components[0].Type, apiSolutionState.Spec.Components[0].Type) + assert.Equal(t, expectedProperties, apiSolutionState.Spec.Components[0].Properties) + + assert.Equal(t, solution.Spec.Components[0].Sidecars[0].Name, apiSolutionState.Spec.Components[0].Sidecars[0].Name) + assert.Equal(t, solution.Spec.Components[0].Sidecars[0].Type, apiSolutionState.Spec.Components[0].Sidecars[0].Type) + assert.Equal(t, expectedSidecarProperties, apiSolutionState.Spec.Components[0].Sidecars[0].Properties) +} diff --git a/packages/helm/symphony/templates/symphony.yaml b/packages/helm/symphony/templates/symphony.yaml index c0156b376..34b0c0622 100644 --- a/packages/helm/symphony/templates/symphony.yaml +++ b/packages/helm/symphony/templates/symphony.yaml @@ -79,7 +79,6 @@ spec: stage: type: string status: - description: State represents a response state type: integer updateTime: type: string @@ -435,7 +434,6 @@ spec: metadata: type: object spec: - description: InstanceSpec defines the spec property of the InstanceState properties: arguments: additionalProperties: @@ -474,6 +472,19 @@ spec: - skill type: object type: array + reconciliationPolicy: + description: Optional ReconcilicationPolicy to specify how target + controller should reconcile. Now only periodic reconciliation is + supported. If the interval is 0, it will only reconcile when the + instance is created or updated. + properties: + interval: + type: string + state: + type: string + required: + - state + type: object scope: type: string solution: @@ -518,15 +529,11 @@ spec: type: object type: object type: array - version: - description: Defines the version of a particular resource - type: string required: - name - solution type: object status: - description: InstanceStatus defines the observed state of Instance properties: lastModified: format: date-time @@ -534,8 +541,6 @@ spec: properties: additionalProperties: type: string - description: 'Important: Run "make" to regenerate code after modifying - this file' type: object provisioningStatus: description: Defines the state of the ARM resource for long running @@ -754,7 +759,6 @@ spec: reason: type: string state: - description: State represents a response state type: integer type: object type: object @@ -768,7 +772,6 @@ spec: reason: type: string state: - description: State represents a response state type: integer type: object type: object @@ -1371,6 +1374,19 @@ spec: additionalProperties: type: string type: object + reconciliationPolicy: + description: Optional ReconcilicationPolicy to specify how target + controller should reconcile. Now only periodic reconciliation is + supported. If the interval is 0, it will only reconcile when the + instance is created or updated. + properties: + interval: + type: string + state: + type: string + required: + - state + type: object scope: type: string topologies: @@ -1402,12 +1418,8 @@ spec: type: object type: object type: array - version: - description: Defines the version of a particular resource - type: string type: object status: - description: TargetStatus defines the observed state of Target properties: lastModified: format: date-time @@ -1415,8 +1427,6 @@ spec: properties: additionalProperties: type: string - description: 'Important: Run "make" to regenerate code after modifying - this file' type: object provisioningStatus: description: Defines the state of the ARM resource for long running From 02bfa813f9fd24f718a89193d8c923ea31abdfd3 Mon Sep 17 00:00:00 2001 From: Xingdong Li Date: Thu, 2 May 2024 00:28:22 +0800 Subject: [PATCH 09/26] Support device in non-default namespace (#238) * Support device in non-default namespace * fix missing argument in test * handle missing namespace field * Fix provisiongingStatus missing error in target update --- .../managers/target/target-manager.go | 27 ++++++++++++------- .../managers/target/target-manager_test.go | 2 +- .../providers/reporter/http/httpprovider.go | 7 ++++- .../providers/reporter/k8s/k8sreporter.go | 20 +++++++++----- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/api/pkg/apis/v1alpha1/managers/target/target-manager.go b/api/pkg/apis/v1alpha1/managers/target/target-manager.go index e1bac3cf0..6fd34a904 100644 --- a/api/pkg/apis/v1alpha1/managers/target/target-manager.go +++ b/api/pkg/apis/v1alpha1/managers/target/target-manager.go @@ -108,7 +108,7 @@ func (s *TargetManager) Poll() []error { target := s.ReferenceProvider.TargetID() log.Infof(" M (Target): Poll target- %s", target) - ret, err := s.ReferenceProvider.List(target+"=true", "", "default", model.FabricGroup, "devices", "v1", "v1alpha2.ReferenceK8sCRD") + ret, err := s.ReferenceProvider.List(target+"=true", "", "", model.FabricGroup, "devices", "v1", "v1alpha2.ReferenceK8sCRD") if err != nil { return []error{err} } @@ -133,6 +133,10 @@ func (s *TargetManager) Poll() []error { ip = i } name := device.Object.Metadata["name"].(string) + namespace, ok := device.Object.Metadata["namespace"].(string) + if !ok { + namespace = "default" + } if ip != "" { if user != "" && password != "" { log.Debugf("taking snapshot from rtsp://%s:%s@%s...", user, "", strings.ReplaceAll(ip, "rtsp://", "")) @@ -143,7 +147,7 @@ func (s *TargetManager) Poll() []error { if err != nil { log.Debugf("failed to probe device: %s", err.Error()) errors = append(errors, err) - errors = append(errors, s.reportStatus(name, target, "", "disconnected", "disconnected", first, err.Error())...) + errors = append(errors, s.reportStatus(name, namespace, target, "", "disconnected", "disconnected", first, err.Error())...) continue } if v, ok := ret["snapshot"]; ok { @@ -151,14 +155,14 @@ func (s *TargetManager) Poll() []error { if err != nil { log.Debugf("failed to open local file: %s", err.Error()) errors = append(errors, err) - errors = append(errors, s.reportStatus(name, target, "", "connected", "connected", first, err.Error())...) + errors = append(errors, s.reportStatus(name, namespace, target, "", "connected", "connected", first, err.Error())...) continue } data, err := ioutil.ReadAll(file) if err != nil { log.Debugf("failed to read local file: %s", err.Error()) errors = append(errors, err) - errors = append(errors, s.reportStatus(name, target, "", "connected", "connected", first, err.Error())...) + errors = append(errors, s.reportStatus(name, namespace, target, "", "connected", "connected", first, err.Error())...) continue } fileName := filepath.Base(v) @@ -166,20 +170,23 @@ func (s *TargetManager) Poll() []error { if err != nil { log.Debugf("failed to upload snapshot: %s", err.Error()) errors = append(errors, err) - errors = append(errors, s.reportStatus(name, target, "", "connected", "connected", first, err.Error())...) + errors = append(errors, s.reportStatus(name, namespace, target, "", "connected", "connected", first, err.Error())...) continue } log.Debugf("file is uploaded to %s", str) - errors = append(errors, s.reportStatus(name, target, str, "connected", "connected", first, "")...) + errors = append(errors, s.reportStatus(name, namespace, target, str, "connected", "connected", first, "")...) } } else { - errors = append(errors, s.reportStatus(name, target, "", "disconnected", "disconnected", first, "device ip is not set")...) + errors = append(errors, s.reportStatus(name, namespace, target, "", "disconnected", "disconnected", first, "device ip is not set")...) } first = false } + for _, err := range errors { + log.Errorf(" M (Target): polling error: %s", err.Error()) + } return errors } -func (s *TargetManager) reportStatus(deviceName string, targetName string, snapshot string, targetStatus string, deviceStatus string, overwrite bool, errStr string) []error { +func (s *TargetManager) reportStatus(deviceName string, namespace string, targetName string, snapshot string, targetStatus string, deviceStatus string, overwrite bool, errStr string) []error { log.Infof(" M (Target): reportStatus deviceName- %s, targetName - %s, snapshot -%s targetStatus -%s, deviceStatus -%s, overwrite -%s", deviceName, targetName, snapshot, targetStatus, deviceStatus, overwrite) ret := make([]error, 0) @@ -191,7 +198,7 @@ func (s *TargetManager) reportStatus(deviceName string, targetName string, snaps if errStr != "" { report[targetName+".err"] = errStr } - err := s.Reporter.Report(deviceName, "default", model.FabricGroup, "devices", "v1", report, false) //can't overwrite device state properties as other targets may be reporting as well + err := s.Reporter.Report(deviceName, namespace, model.FabricGroup, "devices", "v1", report, false) //can't overwrite device state properties as other targets may be reporting as well if err != nil { log.Debugf("failed to report device status: %s", err.Error()) ret = append(ret, err) @@ -201,7 +208,7 @@ func (s *TargetManager) reportStatus(deviceName string, targetName string, snaps if errStr != "" { report[deviceName+".err"] = errStr } - err = s.Reporter.Report(targetName, "default", model.FabricGroup, "targets", "v1", report, overwrite) + err = s.Reporter.Report(targetName, namespace, model.FabricGroup, "targets", "v1", report, overwrite) if err != nil { log.Debugf("failed to report target status: %s", err.Error()) ret = append(ret, err) diff --git a/api/pkg/apis/v1alpha1/managers/target/target-manager_test.go b/api/pkg/apis/v1alpha1/managers/target/target-manager_test.go index 826e37d8a..4fd765199 100644 --- a/api/pkg/apis/v1alpha1/managers/target/target-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/target/target-manager_test.go @@ -75,7 +75,7 @@ func TestReport(t *testing.T) { ReferenceProvider: provider, Reporter: reporter, } - errRep := manager.reportStatus("testDev", "testTar", "testSnapshot", "active", "active", true, "testErr") + errRep := manager.reportStatus("testDev", "default", "testTar", "testSnapshot", "active", "active", true, "testErr") assert.Equal(t, []error{}, errRep) } diff --git a/coa/pkg/apis/v1alpha2/providers/reporter/http/httpprovider.go b/coa/pkg/apis/v1alpha2/providers/reporter/http/httpprovider.go index 9eab81191..2e1cfb466 100644 --- a/coa/pkg/apis/v1alpha2/providers/reporter/http/httpprovider.go +++ b/coa/pkg/apis/v1alpha2/providers/reporter/http/httpprovider.go @@ -98,10 +98,15 @@ func (m *HTTPReporter) Report(id string, namespace string, group string, kind st return err } defer resp.Body.Close() - _, err = ioutil.ReadAll(resp.Body) + var body []byte + body, err = ioutil.ReadAll(resp.Body) if err != nil { return err } + + if resp.StatusCode != http.StatusOK { + return v1alpha2.NewCOAError(nil, string(body), v1alpha2.State(resp.StatusCode)) + } return nil } diff --git a/coa/pkg/apis/v1alpha2/providers/reporter/k8s/k8sreporter.go b/coa/pkg/apis/v1alpha2/providers/reporter/k8s/k8sreporter.go index 88f1170b6..082e4025e 100644 --- a/coa/pkg/apis/v1alpha2/providers/reporter/k8s/k8sreporter.go +++ b/coa/pkg/apis/v1alpha2/providers/reporter/k8s/k8sreporter.go @@ -10,6 +10,7 @@ import ( "context" "encoding/json" "path/filepath" + "time" "strconv" @@ -137,9 +138,11 @@ func (m *K8sReporter) Report(id string, namespace string, group string, kind str propCol := make(map[string]string) - if !overwrtie { - if existingStatus, ok := obj.Object["status"]; ok { - dict := existingStatus.(map[string]interface{}) + statusMap := map[string]interface{}{} + + if existingStatus, ok := obj.Object["status"]; ok { + dict := existingStatus.(map[string]interface{}) + if !overwrtie { if propsElement, ok := dict["properties"]; ok { props := propsElement.(map[string]interface{}) for k, v := range props { @@ -147,11 +150,18 @@ func (m *K8sReporter) Report(id string, namespace string, group string, kind str } } } + if provisioningStatus, ok := dict["provisioningStatus"]; ok { + statusMap["provisioningStatus"] = provisioningStatus + } + if _, ok := dict["lastModified"]; ok { + statusMap["lastModified"] = time.Now().Format(time.RFC3339) + } } for k, v := range properties { propCol[k] = v } + statusMap["properties"] = propCol status := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -160,9 +170,7 @@ func (m *K8sReporter) Report(id string, namespace string, group string, kind str "metadata": map[string]interface{}{ "name": id, }, - "status": map[string]interface{}{ - "properties": propCol, - }, + "status": statusMap, }, } From 4504f6058c0f28a32c61a0d23c03e501993b7e22 Mon Sep 17 00:00:00 2001 From: Haishi2016 Date: Thu, 2 May 2024 10:03:34 -0700 Subject: [PATCH 10/26] mo mod tidy on maestro (#244) --- cli/go.mod | 4 ++-- cli/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index 5f34a0e03..68a972810 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -6,7 +6,7 @@ replace github.com/eclipse-symphony/symphony/api => ../api replace github.com/eclipse-symphony/symphony/coa => ../coa -require github.com/spf13/cobra v1.6.1 +require github.com/spf13/cobra v1.7.0 require ( github.com/eclipse-symphony/symphony/coa v0.0.0 // indirect @@ -26,7 +26,7 @@ require ( require ( github.com/eclipse-symphony/symphony/api v0.0.0 - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedib0t/go-pretty/v6 v6.4.2 github.com/mattn/go-runewidth v0.0.13 // indirect github.com/rivo/uniseg v0.2.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 7e790b1ca..e44d02d6a 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -11,8 +11,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.4.2 h1:DcJNSNIb1E17Tvy9w9S7z+sExvWvvjNbFdyr6C+FUL0= github.com/jedib0t/go-pretty/v6 v6.4.2/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/matryer/is v1.3.0 h1:9qiso3jaJrOe6qBRJRBt2Ldht05qDiFP9le0JOIhRSI= @@ -41,8 +41,8 @@ github.com/princjef/mageutil v1.0.0/go.mod h1:mkShhaUomCYfAoVvTKRcbAs8YSVPdtezI5 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 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= From 9d44b8b52e86cb722c80b4a865c2bc415f6a2740 Mon Sep 17 00:00:00 2001 From: Jiawei Du <59427055+msftcoderdjw@users.noreply.github.com> Date: Mon, 6 May 2024 10:58:45 +0800 Subject: [PATCH 11/26] Remove spec.name in catalog/instance/campaign/activation (#242) * remove catalog.spec.name * remove instance.spec.name * remove spec.name in catalog/instance k8s object * fix catalog webhook * remove spec.name in catalog in docs and tests * remove spec.name in activation * remove spec.name in campaign * fix samples in symphony object in catalog for materializing --------- Co-authored-by: Jiawei Du Co-authored-by: Haishi2016 --- agent/src/main.rs | 8 +-- agent/src/models.rs | 5 +- .../managers/catalogs/catalogs-manager.go | 2 +- .../catalogs/catalogs-manager_test.go | 43 ++++++------- .../v1alpha1/managers/jobs/jobs-manager.go | 4 +- .../managers/jobs/jobs-manager_test.go | 2 - .../managers/solution/solution-manager.go | 16 ++--- .../solution/solution-manager_test.go | 3 +- .../managers/stage/stage-manager_test.go | 19 ------ .../managers/staging/staging-manager.go | 6 +- .../managers/staging/staging-manager_test.go | 1 - .../v1alpha1/managers/sync/sync-manager.go | 2 +- .../managers/sync/sync-manager_test.go | 3 +- api/pkg/apis/v1alpha1/model/campaign.go | 10 --- api/pkg/apis/v1alpha1/model/campaign_test.go | 46 ++------------ api/pkg/apis/v1alpha1/model/catalog.go | 5 -- api/pkg/apis/v1alpha1/model/catalog_test.go | 15 +---- .../apis/v1alpha1/model/deployment_test.go | 63 ++++++++++++------- api/pkg/apis/v1alpha1/model/instance.go | 5 -- api/pkg/apis/v1alpha1/model/instance_test.go | 38 +++++------ .../providers/stage/create/create_test.go | 8 +-- .../v1alpha1/providers/stage/list/list.go | 2 +- .../providers/stage/list/list_test.go | 16 ++--- .../stage/materialize/materialize.go | 14 ++--- .../stage/materialize/materialize_test.go | 11 +--- .../providers/stage/patch/patch_test.go | 1 - .../v1alpha1/providers/stage/wait/wait.go | 4 +- .../providers/stage/wait/wait_test.go | 8 +-- .../v1alpha1/providers/states/k8s/k8s_test.go | 4 -- .../apis/v1alpha1/providers/target/adb/adb.go | 4 +- .../providers/target/azure/adu/adu.go | 4 +- .../providers/target/azure/iotedge/iotedge.go | 12 ++-- .../providers/target/configmap/configmap.go | 4 +- .../target/configmap/configmap_test.go | 8 +-- .../providers/target/docker/docker.go | 6 +- .../v1alpha1/providers/target/helm/helm.go | 6 +- .../providers/target/helm/helm_test.go | 20 ++++-- .../providers/target/helm/postrenderer.go | 2 +- .../v1alpha1/providers/target/http/http.go | 6 +- .../providers/target/ingress/ingress.go | 4 +- .../providers/target/ingress/ingress_test.go | 20 +++--- .../apis/v1alpha1/providers/target/k8s/k8s.go | 20 +++--- .../v1alpha1/providers/target/k8s/k8s_test.go | 10 ++- .../providers/target/kubectl/kubectl.go | 10 +-- .../providers/target/kubectl/kubectl_test.go | 39 ++++++++---- .../providers/target/mock/mock_test.go | 4 +- .../v1alpha1/providers/target/mqtt/mqtt.go | 6 +- .../providers/target/mqtt/mqtt_test.go | 4 +- .../v1alpha1/providers/target/proxy/proxy.go | 4 +- .../providers/target/script/script.go | 4 +- .../providers/target/staging/staging.go | 9 ++- .../providers/target/staging/staging_test.go | 18 ++++-- .../target/win10/sideload/sideload.go | 4 +- .../v1alpha1/utils/metahelper/metahelper.go | 18 +++--- api/pkg/apis/v1alpha1/utils/parser.go | 2 +- api/pkg/apis/v1alpha1/utils/parser_test.go | 9 ++- api/pkg/apis/v1alpha1/utils/symphony-api.go | 4 -- .../apis/v1alpha1/utils/symphony-api_test.go | 2 - .../vendors/activations-vendor_test.go | 1 - .../v1alpha1/vendors/campaigns-vendor_test.go | 12 +--- .../apis/v1alpha1/vendors/catalogs-vendor.go | 3 +- .../v1alpha1/vendors/catalogs-vendor_test.go | 50 +++++++-------- .../v1alpha1/vendors/federation-vendor.go | 6 +- .../vendors/federation-vendor_test.go | 6 +- .../apis/v1alpha1/vendors/instances-vendor.go | 4 -- .../apis/v1alpha1/vendors/solution-vendor.go | 2 +- .../v1alpha1/vendors/solution-vendor_test.go | 6 +- .../v1alpha1/vendors/trails-vendor_test.go | 14 +++-- .../v1alpha1/vendors/visualization-vendor.go | 2 +- .../samples/approval/logicapp/activation.yaml | 3 +- .../approval/logicapp/instance-catalog.yaml | 2 - docs/samples/approval/script/activation.yaml | 3 +- .../approval/script/instance-catalog.yaml | 2 - docs/samples/campaigns/mock/activation.yaml | 1 - .../campaigns/remote-schedule/activation.yaml | 3 +- .../campaigns/scheduled/activation.yaml | 3 +- docs/samples/canary/activation.yaml | 4 +- docs/samples/multisite/activation.yaml | 1 - docs/samples/multisite/catalog-catalog.yaml | 4 +- docs/samples/multisite/instance-catalog.yaml | 1 - docs/samples/multisite/solution-catalog.yaml | 1 - docs/samples/multisite/target-catalog.yaml | 1 - .../piccolo/tiny_stack/activation.yaml | 3 +- .../chemical-factory-2/catalogs/assets.yaml | 9 --- .../catalogs/configurations.yaml | 6 -- .../configurations-with-schema.yaml | 1 - .../chemical-factory/configurations.yaml | 12 +--- .../chemical-factory/manifests.yaml | 5 -- .../chemical-factory/schemas.yaml | 1 - .../universe-data/chemical-factory/sites.yaml | 13 ---- .../concepts/unified-object-model/catalog.md | 3 - .../define-configurations copy.md | 1 - .../define-configurations.md | 1 - .../configuration-management/late-assembly.md | 1 - k8s/apis/federation/v1/catalog_webhook.go | 4 +- k8s/apis/model/v1/common_types.go | 3 - .../bases/federation.symphony_catalogs.yaml | 3 - .../bases/solution.symphony_instances.yaml | 3 - .../bases/workflow.symphony_activations.yaml | 2 - .../federation/catalog_controller.go | 2 +- k8s/utils/symphony-api.go | 1 - .../helm/symphony/templates/symphony.yaml | 8 --- .../04.workflow/manifest/activation.yaml | 1 - .../manifest/instance-catalog.yaml | 1 - .../scenarios/05.catalog/catalogs/asset.yaml | 1 - .../scenarios/05.catalog/catalogs/config.yaml | 1 - .../05.catalog/catalogs/instance.yaml | 1 - .../scenarios/05.catalog/catalogs/schema.yaml | 1 - .../05.catalog/catalogs/solution.yaml | 1 - .../scenarios/05.catalog/catalogs/target.yaml | 1 - .../05.catalog/catalogs/wrongconfig.yaml | 1 - 111 files changed, 333 insertions(+), 520 deletions(-) diff --git a/agent/src/main.rs b/agent/src/main.rs index a6da2c9ca..c45856a43 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -34,7 +34,7 @@ fn main() { let status: ExitStatus = ExitStatus::from_raw(0); match component.component_type.as_str() { "docker" => { - let status = deploy_docker(&catalog.spec.name, &component); + let status = deploy_docker(&catalog.metadata.name, &component); if status.success() { println!("Docker deployment is done."); } else { @@ -42,7 +42,7 @@ fn main() { } }, "wasm" => { - let status = deploy_wasmedge(&catalog.spec.name, &component); + let status = deploy_wasmedge(&catalog.metadata.name, &component); if status.success() { println!("WASM deployment is done."); } else { @@ -50,7 +50,7 @@ fn main() { } }, "ebpf" => { - let status = deploy_ebpf(&catalog.spec.name, &component); + let status = deploy_ebpf(&catalog.metadata.name, &component); if status.success() { println!("eBPF deployment is done."); } else { @@ -68,7 +68,7 @@ fn main() { } } } else { - println!("No components found in catalog {}", catalog.spec.name); + println!("No components found in catalog {}", catalog.metadata.name); } } } diff --git a/agent/src/models.rs b/agent/src/models.rs index 7b57d3239..2e313edfc 100644 --- a/agent/src/models.rs +++ b/agent/src/models.rs @@ -44,7 +44,6 @@ pub struct StagedProperties { pub struct CatalogSpec { #[serde(rename = "siteId")] site_id: String, - pub name: String, #[serde(rename = "type")] pub catalog_type: String, pub properties: StagedProperties, @@ -59,13 +58,13 @@ pub struct CatalogStatus { #[derive(Serialize, Deserialize)] pub struct ObjectMeta { namespace: Option, - name: Option, + pub name: String, labels: Option>, annotations: Option>, } #[derive(Serialize, Deserialize)] pub struct CatalogState { - metadata: ObjectMeta, + pub metadata: ObjectMeta, pub spec: CatalogSpec, status: Option, } \ No newline at end of file diff --git a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go index a4c7e0a51..432250734 100644 --- a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go +++ b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go @@ -179,7 +179,7 @@ func (m *CatalogsManager) UpsertState(ctx context.Context, name string, state mo "objectType": state.Spec.Type, }, Body: v1alpha2.JobData{ - Id: state.Spec.Name, + Id: state.ObjectMeta.Name, Action: v1alpha2.JobUpdate, Body: state, }, diff --git a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go index c043e27e5..a54bda182 100644 --- a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go @@ -33,7 +33,6 @@ var catalogState = model.CatalogState{ }, Spec: &model.CatalogSpec{ SiteId: "site1", - Name: "name1", Type: "catalog", Properties: map[string]interface{}{ "property1": "value1", @@ -82,21 +81,19 @@ func CreateSimpleChain(root string, length int, CTManager CatalogsManager, catal json.Unmarshal(jData, &newCatalog) newCatalog.ObjectMeta.Name = root - newCatalog.Spec.Name = root newCatalog.Spec.ParentName = "" - err := CTManager.UpsertState(context.Background(), newCatalog.Spec.Name, newCatalog) + err := CTManager.UpsertState(context.Background(), newCatalog.ObjectMeta.Name, newCatalog) if err != nil { return err } for i := 1; i < length; i++ { - tmp := newCatalog.Spec.Name + tmp := newCatalog.ObjectMeta.Name var childCatalog model.CatalogState jData, _ := json.Marshal(newCatalog) json.Unmarshal(jData, &childCatalog) - childCatalog.Spec.Name = fmt.Sprintf("%s-%d", root, i) + childCatalog.ObjectMeta.Name = fmt.Sprintf("%s-%d", root, i) childCatalog.Spec.ParentName = tmp - childCatalog.ObjectMeta.Name = childCatalog.Spec.Name - err := CTManager.UpsertState(context.Background(), childCatalog.Spec.Name, childCatalog) + err := CTManager.UpsertState(context.Background(), childCatalog.ObjectMeta.Name, childCatalog) if err != nil { return err } @@ -114,10 +111,9 @@ func CreateSimpleBinaryTree(root string, depth int, CTManager CatalogsManager, c jData, _ := json.Marshal(catalog) json.Unmarshal(jData, &newCatalog) - newCatalog.Spec.Name = fmt.Sprintf("%s-%d", root, 0) - newCatalog.ObjectMeta.Name = newCatalog.Spec.Name + newCatalog.ObjectMeta.Name = fmt.Sprintf("%s-%d", root, 0) newCatalog.Spec.ParentName = "" - err := CTManager.UpsertState(context.Background(), newCatalog.Spec.Name, newCatalog) + err := CTManager.UpsertState(context.Background(), newCatalog.ObjectMeta.Name, newCatalog) if err != nil { return err } @@ -129,10 +125,9 @@ func CreateSimpleBinaryTree(root string, depth int, CTManager CatalogsManager, c var childCatalog model.CatalogState jData, _ := json.Marshal(newCatalog) json.Unmarshal(jData, &childCatalog) - childCatalog.Spec.Name = fmt.Sprintf("%s-%d", root, count) - childCatalog.ObjectMeta.Name = childCatalog.Spec.Name + childCatalog.ObjectMeta.Name = fmt.Sprintf("%s-%d", root, count) childCatalog.Spec.ParentName = fmt.Sprintf("%s-%d", root, parentIndex) - err := CTManager.UpsertState(context.Background(), childCatalog.Spec.Name, childCatalog) + err := CTManager.UpsertState(context.Background(), childCatalog.ObjectMeta.Name, childCatalog) if err != nil { return err } @@ -152,7 +147,7 @@ func TestUpsertAndGet(t *testing.T) { err := initalizeManager() assert.Nil(t, err) - err = manager.UpsertState(context.Background(), catalogState.Spec.Name, catalogState) + err = manager.UpsertState(context.Background(), catalogState.ObjectMeta.Name, catalogState) assert.Nil(t, err) manager.Context.Subscribe("catalog", func(topic string, event v1alpha2.Event) error { var job v1alpha2.JobData @@ -164,7 +159,7 @@ func TestUpsertAndGet(t *testing.T) { assert.Equal(t, true, job.Action == v1alpha2.JobUpdate || job.Action == v1alpha2.JobDelete) return nil }) - val, err := manager.GetState(context.Background(), catalogState.Spec.Name, catalogState.ObjectMeta.Namespace) + val, err := manager.GetState(context.Background(), catalogState.ObjectMeta.Name, catalogState.ObjectMeta.Namespace) assert.Nil(t, err) equal, err := catalogState.DeepEquals(val) assert.Nil(t, err) @@ -175,7 +170,7 @@ func TestList(t *testing.T) { err := initalizeManager() assert.Nil(t, err) - err = manager.UpsertState(context.Background(), catalogState.Spec.Name, catalogState) + err = manager.UpsertState(context.Background(), catalogState.ObjectMeta.Name, catalogState) assert.Nil(t, err) manager.Context.Subscribe("catalog", func(topic string, event v1alpha2.Event) error { var job v1alpha2.JobData @@ -199,7 +194,7 @@ func TestDelete(t *testing.T) { err := initalizeManager() assert.Nil(t, err) - err = manager.UpsertState(context.Background(), catalogState.Spec.Name, catalogState) + err = manager.UpsertState(context.Background(), catalogState.ObjectMeta.Name, catalogState) assert.Nil(t, err) manager.Context.Subscribe("catalog", func(topic string, event v1alpha2.Event) error { var job v1alpha2.JobData @@ -211,16 +206,16 @@ func TestDelete(t *testing.T) { assert.Equal(t, true, job.Action == v1alpha2.JobUpdate || job.Action == v1alpha2.JobDelete) return nil }) - val, err := manager.GetState(context.Background(), catalogState.Spec.Name, catalogState.ObjectMeta.Namespace) + val, err := manager.GetState(context.Background(), catalogState.ObjectMeta.Name, catalogState.ObjectMeta.Namespace) assert.Nil(t, err) equal, err := catalogState.DeepEquals(val) assert.Nil(t, err) assert.True(t, equal) - err = manager.DeleteState(context.Background(), catalogState.Spec.Name, catalogState.ObjectMeta.Namespace) + err = manager.DeleteState(context.Background(), catalogState.ObjectMeta.Name, catalogState.ObjectMeta.Namespace) assert.Nil(t, err) - val, err = manager.GetState(context.Background(), catalogState.Spec.Name, catalogState.ObjectMeta.Namespace) + val, err = manager.GetState(context.Background(), catalogState.ObjectMeta.Name, catalogState.ObjectMeta.Namespace) assert.NotNil(t, err) assert.Empty(t, val) } @@ -237,7 +232,7 @@ func TestGetChains(t *testing.T) { tk, err := manager.ListState(context.Background(), catalogState.ObjectMeta.Namespace, "", "") assert.Nil(t, err) for _, v := range tk { - fmt.Println(v.Spec.Name) + fmt.Println(v.ObjectMeta.Name) } val, err := manager.GetChains(context.Background(), catalogState.Spec.Type, catalogState.ObjectMeta.Namespace) @@ -273,15 +268,13 @@ func TestSchemaCheck(t *testing.T) { catalogState.Spec.Properties = map[string]interface{}{ "spec": schema, } - catalogState.Spec.Name = "EmailCheckSchema" catalogState.Spec.ParentName = "" catalogState.ObjectMeta = model.ObjectMeta{ Name: "EmailCheckSchema", } - err = manager.UpsertState(context.Background(), catalogState.Spec.Name, catalogState) + err = manager.UpsertState(context.Background(), catalogState.ObjectMeta.Name, catalogState) assert.Nil(t, err) - catalogState.Spec.Name = "Email" catalogState.Spec.Metadata = map[string]string{ "schema": "EmailCheckSchema", } @@ -292,7 +285,7 @@ func TestSchemaCheck(t *testing.T) { "email": "This is an invalid email", } - err = manager.UpsertState(context.Background(), catalogState.Spec.Name, catalogState) + err = manager.UpsertState(context.Background(), catalogState.ObjectMeta.Name, catalogState) assert.NotNil(t, err) assert.True(t, strings.Contains(err.Error(), "schema validation error")) } diff --git a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go index 1b5cecbc0..f365a00fa 100644 --- a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go +++ b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go @@ -445,7 +445,7 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) if err != nil { return err } else { - return s.apiClient.DeleteInstance(ctx, deployment.Instance.Spec.Name, namespace) + return s.apiClient.DeleteInstance(ctx, deployment.Instance.ObjectMeta.Name, namespace) } default: return v1alpha2.NewCOAError(nil, "unsupported action", v1alpha2.BadRequest) @@ -508,7 +508,7 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) // TODO: how to handle status updates? s.StateProvider.Upsert(ctx, states.UpsertRequest{ Value: states.StateEntry{ - ID: "d_" + deployment.Instance.Spec.Name, + ID: "d_" + deployment.Instance.ObjectMeta.Name, Body: LastSuccessTime{ Time: time.Now().UTC(), }, diff --git a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager_test.go b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager_test.go index 0a4817a60..c7347ec19 100644 --- a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager_test.go @@ -238,7 +238,6 @@ func InitializeMockSymphonyAPI() *httptest.Server { Namespace: "default", }, Spec: &model.InstanceSpec{ - Name: "instance1", Solution: "solution1", }, } @@ -249,7 +248,6 @@ func InitializeMockSymphonyAPI() *httptest.Server { Namespace: "default", }, Spec: &model.InstanceSpec{ - Name: "instance1", Solution: "solution1", }, }} diff --git a/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go b/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go index f3ac65442..c09d1dad6 100644 --- a/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go +++ b/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go @@ -223,7 +223,7 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy stopCh := make(chan struct{}) defer close(stopCh) - go s.sendHeartbeat(deployment.Instance.Spec.Name, namespace, remove, stopCh) + go s.sendHeartbeat(deployment.Instance.ObjectMeta.Name, namespace, remove, stopCh) iCtx, span := observability.StartSpan("Solution Manager", ctx, &map[string]string{ "method": "Reconcile", @@ -232,7 +232,7 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy defer observ_utils.CloseSpanWithError(span, &err) log.Infof(" M (Solution): reconciling deployment.InstanceName: %s, deployment.SolutionName: %s, remove: %t, namespace: %s, targetName: %s, traceId: %s", - deployment.Instance.Spec.Name, + deployment.Instance.ObjectMeta.Name, deployment.SolutionName, remove, namespace, @@ -284,7 +284,7 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy } } - previousDesiredState := s.getPreviousState(iCtx, deployment.Instance.Spec.Name, namespace) + previousDesiredState := s.getPreviousState(iCtx, deployment.Instance.ObjectMeta.Name, namespace) var currentDesiredState, currentState model.DeploymentState currentDesiredState, err = NewDeploymentState(deployment) @@ -447,7 +447,7 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy // if len(mergedState.TargetComponent) == 0 { // log.Infof(" M (Solution): no assigned components to manage, deleting state") // s.StateProvider.Delete(iCtx, states.DeleteRequest{ - // ID: deployment.Instance.Spec.Name, + // ID: deployment.Instance.ObjectMeta.Name, // Metadata: map[string]interface{}{ // "namespace": namespace, // }, @@ -455,7 +455,7 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy // } else { s.StateProvider.Upsert(iCtx, states.UpsertRequest{ Value: states.StateEntry{ - ID: deployment.Instance.Spec.Name, + ID: deployment.Instance.ObjectMeta.Name, Body: SolutionManagerDeploymentState{ Spec: deployment, State: mergedState, @@ -501,7 +501,7 @@ func (s *SolutionManager) saveSummary(ctx context.Context, deployment model.Depl // TODO: delete this state when time expires. This should probably be invoked by the vendor (via GetSummary method, for instance) s.StateProvider.Upsert(ctx, states.UpsertRequest{ Value: states.StateEntry{ - ID: fmt.Sprintf("%s-%s", "summary", deployment.Instance.Spec.Name), + ID: fmt.Sprintf("%s-%s", "summary", deployment.Instance.ObjectMeta.Name), Body: model.SummaryResult{ Summary: summary, Generation: deployment.Generation, @@ -561,7 +561,7 @@ func (s *SolutionManager) Get(ctx context.Context, deployment model.DeploymentSp var err error = nil defer observ_utils.CloseSpanWithError(span, &err) log.Infof(" M (Solution): getting deployment.InstanceName: %s, deployment.SolutionName: %s, targetName: %s, traceId: %s", - deployment.Instance.Spec.Name, + deployment.Instance.ObjectMeta.Name, deployment.SolutionName, targetName, span.SpanContext().TraceID().String()) @@ -679,7 +679,7 @@ func (s *SolutionManager) Poll() []error { if err != nil { return []error{err} } - err = api_utils.ReportCatalogs(context.Background(), symphonyUrl, s.Context.SiteInfo.ParentSite.Username, s.Context.SiteInfo.ParentSite.Password, deployment.Instance.Spec.Name+"-"+target, components) + err = api_utils.ReportCatalogs(context.Background(), symphonyUrl, s.Context.SiteInfo.ParentSite.Username, s.Context.SiteInfo.ParentSite.Password, deployment.Instance.ObjectMeta.Name+"-"+target, components) if err != nil { return []error{err} } diff --git a/api/pkg/apis/v1alpha1/managers/solution/solution-manager_test.go b/api/pkg/apis/v1alpha1/managers/solution/solution-manager_test.go index 2103f63b8..21876a3a6 100644 --- a/api/pkg/apis/v1alpha1/managers/solution/solution-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/solution/solution-manager_test.go @@ -284,9 +284,10 @@ func TestMockGetTwoTargets(t *testing.T) { id := uuid.New().String() deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "instance", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ diff --git a/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go b/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go index b87b000bc..1638576f3 100644 --- a/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go @@ -54,7 +54,6 @@ func TestCampaignWithSingleMockStageLoop(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -115,7 +114,6 @@ func TestCampaignWithSingleCounterStageLoop(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -173,7 +171,6 @@ func TestCampaignWithSingleMegativeCounterStageLoop(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -232,7 +229,6 @@ func TestCampaignWithTwoCounterStageLoop(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -292,7 +288,6 @@ func TestCampaignWithHTTPCounterStageLoop(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -358,7 +353,6 @@ func TestCampaignWithDelay(t *testing.T) { timeStamp := time.Now().UTC() for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -415,7 +409,6 @@ func TestErrorHandler(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -479,7 +472,6 @@ func TestErrorHandlerNotSet(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -535,7 +527,6 @@ func TestAccessingPreviousStage(t *testing.T) { } for { _, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -590,7 +581,6 @@ func TestAccessingStageStatus(t *testing.T) { var status model.ActivationStatus for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -646,7 +636,6 @@ func TestIntentionalError(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -701,7 +690,6 @@ func TestIntentionalErrorState(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -756,7 +744,6 @@ func TestIntentionalErrorString(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -810,7 +797,6 @@ func TestIntentionalErrorStringProper(t *testing.T) { } for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -866,7 +852,6 @@ func TestAccessingPreviousStageInExpression(t *testing.T) { var status model.ActivationStatus for { status, activation = manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -940,7 +925,6 @@ func TestResumeStage(t *testing.T) { Outputs: output, } campaign := model.CampaignSpec{ - Name: campaignName, SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -1006,7 +990,6 @@ func TestResumeStageFailed(t *testing.T) { Outputs: output, } campaign := model.CampaignSpec{ - Name: campaignName, SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -1146,7 +1129,6 @@ func TestHandleActivationEvent(t *testing.T) { } campaign := model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ @@ -1224,7 +1206,6 @@ func TestTriggerEventWithSchedule(t *testing.T) { } status, _ := manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ - Name: "test-campaign", SelfDriving: true, FirstStage: "test", Stages: map[string]model.StageSpec{ diff --git a/api/pkg/apis/v1alpha1/managers/staging/staging-manager.go b/api/pkg/apis/v1alpha1/managers/staging/staging-manager.go index c1572467c..011fc444c 100644 --- a/api/pkg/apis/v1alpha1/managers/staging/staging-manager.go +++ b/api/pkg/apis/v1alpha1/managers/staging/staging-manager.go @@ -103,10 +103,10 @@ func (s *StagingManager) Poll() []error { continue } if err != nil && !v1alpha2.IsNotFound(err) { - log.Errorf(" M (Staging): Failed to get catalog %s: %s", catalog.Spec.Name, err.Error()) + log.Errorf(" M (Staging): Failed to get catalog %s: %s", catalog.ObjectMeta.Name, err.Error()) } s.QueueProvider.Enqueue(siteId, v1alpha2.JobData{ - Id: catalog.Spec.Name, + Id: catalog.ObjectMeta.Name, Action: v1alpha2.JobUpdate, Body: catalog, }) @@ -123,7 +123,7 @@ func (s *StagingManager) Poll() []error { }, }) if err != nil { - log.Errorf(" M (Staging): Failed to record catalog %s: %s", catalog.Spec.Name, err.Error()) + log.Errorf(" M (Staging): Failed to record catalog %s: %s", catalog.ObjectMeta.Name, err.Error()) } } return nil diff --git a/api/pkg/apis/v1alpha1/managers/staging/staging-manager_test.go b/api/pkg/apis/v1alpha1/managers/staging/staging-manager_test.go index b08481362..f8864c4bd 100644 --- a/api/pkg/apis/v1alpha1/managers/staging/staging-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/staging/staging-manager_test.go @@ -155,7 +155,6 @@ func InitializeMockSymphonyAPI() *httptest.Server { Name: "catalog1", }, Spec: &model.CatalogSpec{ - Name: "catalog1", SiteId: "fake", Generation: "1", ParentName: "fakeparent", diff --git a/api/pkg/apis/v1alpha1/managers/sync/sync-manager.go b/api/pkg/apis/v1alpha1/managers/sync/sync-manager.go index 104aaff4c..e4fa6638b 100644 --- a/api/pkg/apis/v1alpha1/managers/sync/sync-manager.go +++ b/api/pkg/apis/v1alpha1/managers/sync/sync-manager.go @@ -61,7 +61,7 @@ func (s *SyncManager) Poll() []error { "origin": batch.Origin, }, Body: v1alpha2.JobData{ - Id: catalog.Spec.Name, + Id: catalog.ObjectMeta.Name, Action: v1alpha2.JobUpdate, //TODO: handle deletion, this probably requires BetBachForSites return flags Body: catalog, }, diff --git a/api/pkg/apis/v1alpha1/managers/sync/sync-manager_test.go b/api/pkg/apis/v1alpha1/managers/sync/sync-manager_test.go index d88bfab6c..61b915e7d 100644 --- a/api/pkg/apis/v1alpha1/managers/sync/sync-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/sync/sync-manager_test.go @@ -116,7 +116,6 @@ func InitiazlizeMockSymphonyAPI(siteId string) *httptest.Server { }, Spec: &model.CatalogSpec{ SiteId: "parent", - Name: "catalog1", Type: "Instance", Properties: map[string]interface{}{ "foo": "bar", @@ -200,6 +199,6 @@ func TestPoll(t *testing.T) { <-sig2 assert.Equal(t, 1, catalogCnt) assert.Equal(t, 1, jobCount) - assert.Equal(t, "catalog1", catalog1.Spec.Name) + assert.Equal(t, "catalog1", catalog1.ObjectMeta.Name) assert.Equal(t, "job1", job1.Id) } diff --git a/api/pkg/apis/v1alpha1/model/campaign.go b/api/pkg/apis/v1alpha1/model/campaign.go index b59238471..474eec48f 100644 --- a/api/pkg/apis/v1alpha1/model/campaign.go +++ b/api/pkg/apis/v1alpha1/model/campaign.go @@ -81,7 +81,6 @@ type ActivationStatus struct { type ActivationSpec struct { Campaign string `json:"campaign,omitempty"` - Name string `json:"name,omitempty"` Stage string `json:"stage,omitempty"` Inputs map[string]interface{} `json:"inputs,omitempty"` Generation string `json:"generation,omitempty"` @@ -97,10 +96,6 @@ func (c ActivationSpec) DeepEquals(other IDeepEquals) (bool, error) { return false, nil } - if c.Name != otherC.Name { - return false, nil - } - if c.Stage != otherC.Stage { return false, nil } @@ -130,7 +125,6 @@ func (c ActivationState) DeepEquals(other IDeepEquals) (bool, error) { } type CampaignSpec struct { - Name string `json:"name,omitempty"` FirstStage string `json:"firstStage,omitempty"` Stages map[string]StageSpec `json:"stages,omitempty"` SelfDriving bool `json:"selfDriving,omitempty"` @@ -142,10 +136,6 @@ func (c CampaignSpec) DeepEquals(other IDeepEquals) (bool, error) { return false, errors.New("parameter is not a CampaignSpec type") } - if c.Name != otherC.Name { - return false, nil - } - if c.FirstStage != otherC.FirstStage { return false, nil } diff --git a/api/pkg/apis/v1alpha1/model/campaign_test.go b/api/pkg/apis/v1alpha1/model/campaign_test.go index e653a35b0..9e29dd099 100644 --- a/api/pkg/apis/v1alpha1/model/campaign_test.go +++ b/api/pkg/apis/v1alpha1/model/campaign_test.go @@ -15,7 +15,6 @@ import ( func TestCampaignMatch(t *testing.T) { campaign1 := CampaignSpec{ - Name: "name", FirstStage: "list", SelfDriving: true, Stages: map[string]StageSpec{ @@ -36,7 +35,6 @@ func TestCampaignMatch(t *testing.T) { }, } campaign2 := CampaignSpec{ - Name: "name", FirstStage: "list", SelfDriving: true, Stages: map[string]StageSpec{ @@ -62,33 +60,17 @@ func TestCampaignMatch(t *testing.T) { } func TestCampaignMatchOneEmpty(t *testing.T) { - campaign1 := CampaignSpec{ - Name: "name", - } + campaign1 := CampaignSpec{} res, err := campaign1.DeepEquals(nil) assert.EqualError(t, err, "parameter is not a CampaignSpec type") assert.False(t, res) } -func TestCampaignNameNotMatch(t *testing.T) { - campaign1 := CampaignSpec{ - Name: "name", - } - campaign2 := CampaignSpec{ - Name: "name1", - } - equal, err := campaign1.DeepEquals(campaign2) - assert.Nil(t, err) - assert.False(t, equal) -} - func TestCampaignFirstStageNotMatch(t *testing.T) { campaign1 := CampaignSpec{ - Name: "name", FirstStage: "list", } campaign2 := CampaignSpec{ - Name: "name", FirstStage: "list1", } equal, err := campaign1.DeepEquals(campaign2) @@ -98,12 +80,10 @@ func TestCampaignFirstStageNotMatch(t *testing.T) { func TestCampaignSelfDrivingNotMatch(t *testing.T) { campaign1 := CampaignSpec{ - Name: "name", FirstStage: "list", SelfDriving: true, } campaign2 := CampaignSpec{ - Name: "name", FirstStage: "list", SelfDriving: false, } @@ -114,7 +94,6 @@ func TestCampaignSelfDrivingNotMatch(t *testing.T) { func TestCampaignStagesLengthNotMatch(t *testing.T) { campaign1 := CampaignSpec{ - Name: "name", FirstStage: "mock1", SelfDriving: true, Stages: map[string]StageSpec{ @@ -125,7 +104,6 @@ func TestCampaignStagesLengthNotMatch(t *testing.T) { }, } campaign2 := CampaignSpec{ - Name: "name", FirstStage: "mock1", SelfDriving: true, Stages: map[string]StageSpec{ @@ -146,7 +124,6 @@ func TestCampaignStagesLengthNotMatch(t *testing.T) { func TestCampaignStagesNotMatch(t *testing.T) { campaign1 := CampaignSpec{ - Name: "name", FirstStage: "mock1", SelfDriving: true, Stages: map[string]StageSpec{ @@ -157,7 +134,6 @@ func TestCampaignStagesNotMatch(t *testing.T) { }, } campaign2 := CampaignSpec{ - Name: "name", FirstStage: "mock1", SelfDriving: true, Stages: map[string]StageSpec{ @@ -266,9 +242,7 @@ func TestStageMatchOneEmpty(t *testing.T) { } func TestActivationMatchOneEmpty(t *testing.T) { - activation1 := ActivationSpec{ - Name: "name", - } + activation1 := ActivationSpec{} res, err := activation1.DeepEquals(nil) assert.EqualError(t, err, "parameter is not a ActivationSpec type") assert.False(t, res) @@ -276,7 +250,6 @@ func TestActivationMatchOneEmpty(t *testing.T) { func TestActivationMatch(t *testing.T) { activation1 := ActivationSpec{ - Name: "multisite-deploy", Campaign: "site-apps", Stage: "deploy", Inputs: map[string]interface{}{ @@ -284,7 +257,6 @@ func TestActivationMatch(t *testing.T) { }, } activation2 := ActivationSpec{ - Name: "multisite-deploy", Campaign: "site-apps", Stage: "deploy", Inputs: map[string]interface{}{ @@ -298,22 +270,14 @@ func TestActivationMatch(t *testing.T) { func TestActivationNotMatch(t *testing.T) { activation1 := ActivationSpec{ - Name: "multisite-deploy", + Campaign: "site-apps", } activation2 := ActivationSpec{ - Name: "multisite-deploy2", + Campaign: "site-apps2", } - // name not match - equal, err := activation1.DeepEquals(activation2) - assert.Nil(t, err) - assert.False(t, equal) - // compaign not match - activation2.Name = "multisite-deploy" - activation1.Campaign = "site-apps" - activation2.Campaign = "site-apps2" - equal, err = activation1.DeepEquals(activation2) + equal, err := activation1.DeepEquals(activation2) assert.Nil(t, err) assert.False(t, equal) diff --git a/api/pkg/apis/v1alpha1/model/catalog.go b/api/pkg/apis/v1alpha1/model/catalog.go index 20209d795..a311e0377 100644 --- a/api/pkg/apis/v1alpha1/model/catalog.go +++ b/api/pkg/apis/v1alpha1/model/catalog.go @@ -32,7 +32,6 @@ type ObjectRef struct { } type CatalogSpec struct { SiteId string `json:"siteId"` - Name string `json:"name"` Type string `json:"type"` Metadata map[string]string `json:"metadata,omitempty"` Properties map[string]interface{} `json:"properties"` @@ -55,10 +54,6 @@ func (c CatalogSpec) DeepEquals(other IDeepEquals) (bool, error) { return false, nil } - if c.Name != otherC.Name { - return false, nil - } - if c.ParentName != otherC.ParentName { return false, nil } diff --git a/api/pkg/apis/v1alpha1/model/catalog_test.go b/api/pkg/apis/v1alpha1/model/catalog_test.go index dca6db904..50bb0d81e 100644 --- a/api/pkg/apis/v1alpha1/model/catalog_test.go +++ b/api/pkg/apis/v1alpha1/model/catalog_test.go @@ -30,7 +30,6 @@ func TestIntefaceConvertion(t *testing.T) { } func TestCatalogMatch(t *testing.T) { catalog1 := CatalogSpec{ - Name: "name", SiteId: "siteId", ParentName: "parentName", Generation: "1", @@ -39,7 +38,6 @@ func TestCatalogMatch(t *testing.T) { }, } catalog2 := CatalogSpec{ - Name: "name", SiteId: "siteId", ParentName: "parentName", Generation: "1", @@ -54,7 +52,6 @@ func TestCatalogMatch(t *testing.T) { func TestCatalogMatchOneEmpty(t *testing.T) { catalog1 := CatalogSpec{ - Name: "name", Type: "type", Properties: map[string]interface{}{ "key": "value", @@ -67,22 +64,16 @@ func TestCatalogMatchOneEmpty(t *testing.T) { func TestCatalogNotMatch(t *testing.T) { catalog1 := CatalogSpec{ - Name: "name", + SiteId: "siteId", } catalog2 := CatalogSpec{ - Name: "name2", + SiteId: "siteId2", } - // name not match - equal, err := catalog1.DeepEquals(catalog2) - assert.Nil(t, err) - assert.False(t, equal) - // siteId not match - catalog2.Name = "name" catalog1.SiteId = "siteId" catalog2.SiteId = "siteId2" - equal, err = catalog1.DeepEquals(catalog2) + equal, err := catalog1.DeepEquals(catalog2) assert.Nil(t, err) assert.False(t, equal) diff --git a/api/pkg/apis/v1alpha1/model/deployment_test.go b/api/pkg/apis/v1alpha1/model/deployment_test.go index 074c3cbce..4aac55786 100644 --- a/api/pkg/apis/v1alpha1/model/deployment_test.go +++ b/api/pkg/apis/v1alpha1/model/deployment_test.go @@ -21,9 +21,10 @@ func TestDeploymentDeepEquals(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": TargetState{ @@ -50,9 +51,10 @@ func TestDeploymentDeepEquals(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": TargetState{ @@ -85,9 +87,10 @@ func TestDeploymentDeepEqualsOneEmpty(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": TargetState{ @@ -120,9 +123,10 @@ func TestDeploymentDeepEqualsSolutionNameNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -149,9 +153,10 @@ func TestDeploymentDeepEqualsSolutionNameNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -184,9 +189,10 @@ func TestDeploymentDeepEqualsSolutionNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -213,9 +219,10 @@ func TestDeploymentDeepEqualsSolutionNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -248,9 +255,10 @@ func TestDeploymentDeepEqualsInstanceNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -277,9 +285,10 @@ func TestDeploymentDeepEqualsInstanceNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName1", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -312,9 +321,10 @@ func TestDeploymentDeepEqualsTargetsNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -341,9 +351,10 @@ func TestDeploymentDeepEqualsTargetsNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo1": { @@ -376,9 +387,10 @@ func TestDeploymentDeepEqualsDevicesNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -405,9 +417,10 @@ func TestDeploymentDeepEqualsDevicesNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -440,9 +453,10 @@ func TestDeploymentDeepEqualsComponentStartIndexNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -469,9 +483,10 @@ func TestDeploymentDeepEqualsComponentStartIndexNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -504,9 +519,10 @@ func TestDeploymentDeepEqualsComponentEndIndexNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -533,9 +549,10 @@ func TestDeploymentDeepEqualsComponentEndIndexNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -568,9 +585,10 @@ func TestDeploymentDeepEqualsActiveTargetNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -597,9 +615,10 @@ func TestDeploymentDeepEqualsActiveTargetNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -661,9 +680,10 @@ func TestDeploymentDeepEqualsAssignmentsNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { @@ -690,9 +710,10 @@ func TestDeploymentDeepEqualsAssignmentsNotMatch(t *testing.T) { }, }, Instance: InstanceState{ - Spec: &InstanceSpec{ + ObjectMeta: ObjectMeta{ Name: "InstanceName", }, + Spec: &InstanceSpec{}, }, Targets: map[string]TargetState{ "foo": { diff --git a/api/pkg/apis/v1alpha1/model/instance.go b/api/pkg/apis/v1alpha1/model/instance.go index fce9f3c90..66e983a3b 100644 --- a/api/pkg/apis/v1alpha1/model/instance.go +++ b/api/pkg/apis/v1alpha1/model/instance.go @@ -23,7 +23,6 @@ type ( // InstanceSpec defines the spec property of the InstanceState // +kubebuilder:object:generate=true InstanceSpec struct { - Name string `json:"name"` DisplayName string `json:"displayName,omitempty"` Scope string `json:"scope,omitempty"` Parameters map[string]string `json:"parameters,omitempty"` //TODO: Do we still need this? @@ -117,10 +116,6 @@ func (c InstanceSpec) DeepEquals(other IDeepEquals) (bool, error) { return false, errors.New("parameter is not a InstanceSpec type") } - if c.Name != otherC.Name { - return false, nil - } - if c.DisplayName != otherC.DisplayName { return false, nil } diff --git a/api/pkg/apis/v1alpha1/model/instance_test.go b/api/pkg/apis/v1alpha1/model/instance_test.go index c4e97139d..b41c8f606 100644 --- a/api/pkg/apis/v1alpha1/model/instance_test.go +++ b/api/pkg/apis/v1alpha1/model/instance_test.go @@ -16,9 +16,9 @@ func TestInstanceDeepEquals(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -38,9 +38,9 @@ func TestInstanceDeepEquals(t *testing.T) { other := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -66,9 +66,9 @@ func TestInstanceDeepEqualsOneEmpty(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -97,9 +97,9 @@ func TestInstanceDeepEqualsNameNotMatch(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -122,9 +122,9 @@ func TestInstanceDeepEqualsNameNotMatch(t *testing.T) { other := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName1", }, Spec: &InstanceSpec{ - Name: "InstanceName1", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -153,9 +153,9 @@ func TestInstanceDeepEqualsDisplayNameNotMatch(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -178,9 +178,9 @@ func TestInstanceDeepEqualsDisplayNameNotMatch(t *testing.T) { other := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName1", Solution: "SolutionName", Target: TargetSelector{ @@ -209,9 +209,9 @@ func TestInstanceDeepEqualsNamespaceNotMatch(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -234,9 +234,9 @@ func TestInstanceDeepEqualsNamespaceNotMatch(t *testing.T) { other := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default1", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -265,9 +265,9 @@ func TestInstanceDeepEqualsTargetNameNotMatch(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -290,9 +290,9 @@ func TestInstanceDeepEqualsTargetNameNotMatch(t *testing.T) { other := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -321,9 +321,9 @@ func TestInstanceDeepEqualsTopologiestNameNotMatch(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -346,9 +346,9 @@ func TestInstanceDeepEqualsTopologiestNameNotMatch(t *testing.T) { other := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -377,9 +377,9 @@ func TestInstanceEqualsPipelineNameNotMatch(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -402,9 +402,9 @@ func TestInstanceEqualsPipelineNameNotMatch(t *testing.T) { other := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -433,9 +433,9 @@ func TestInstanceEqualsArgumentsKeysNotMatch(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -458,9 +458,9 @@ func TestInstanceEqualsArgumentsKeysNotMatch(t *testing.T) { other := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -489,9 +489,9 @@ func TestInstanceEqualsArgumentsValuesNotMatch(t *testing.T) { Instance := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ @@ -514,9 +514,9 @@ func TestInstanceEqualsArgumentsValuesNotMatch(t *testing.T) { other := InstanceState{ ObjectMeta: ObjectMeta{ Namespace: "Default", + Name: "InstanceName", }, Spec: &InstanceSpec{ - Name: "InstanceName", DisplayName: "InstanceDisplayName", Solution: "SolutionName", Target: TargetSelector{ diff --git a/api/pkg/apis/v1alpha1/providers/stage/create/create_test.go b/api/pkg/apis/v1alpha1/providers/stage/create/create_test.go index 95944c483..44a1346e2 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/create/create_test.go +++ b/api/pkg/apis/v1alpha1/providers/stage/create/create_test.go @@ -235,9 +235,7 @@ func InitializeMockSymphonyAPI() *httptest.Server { ObjectMeta: model.ObjectMeta{ Name: "instance1", }, - Spec: &model.InstanceSpec{ - Name: "instance1", - }, + Spec: &model.InstanceSpec{}, Status: model.InstanceStatus{}, } case "/solution/queue": @@ -270,9 +268,7 @@ func InitializeMockSymphonyAPIFailedCase() *httptest.Server { ObjectMeta: model.ObjectMeta{ Name: "instance1", }, - Spec: &model.InstanceSpec{ - Name: "instance1", - }, + Spec: &model.InstanceSpec{}, Status: model.InstanceStatus{}, } case "/solution/queue": diff --git a/api/pkg/apis/v1alpha1/providers/stage/list/list.go b/api/pkg/apis/v1alpha1/providers/stage/list/list.go index 5a21759f7..7459fe3d1 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/list/list.go +++ b/api/pkg/apis/v1alpha1/providers/stage/list/list.go @@ -125,7 +125,7 @@ func (i *ListStageProvider) Process(ctx context.Context, mgrContext contexts.Man if namesOnly { names := make([]string, 0) for _, instance := range instances { - names = append(names, instance.Spec.Name) + names = append(names, instance.ObjectMeta.Name) } outputs["items"] = names } else { diff --git a/api/pkg/apis/v1alpha1/providers/stage/list/list_test.go b/api/pkg/apis/v1alpha1/providers/stage/list/list_test.go index bcd1f52ee..5cfd22ce1 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/list/list_test.go +++ b/api/pkg/apis/v1alpha1/providers/stage/list/list_test.go @@ -192,18 +192,14 @@ func InitializeMockSymphonyAPI() *httptest.Server { ObjectMeta: model.ObjectMeta{ Name: "instance1", }, - Spec: &model.InstanceSpec{ - Name: "instance1", - }, + Spec: &model.InstanceSpec{}, Status: model.InstanceStatus{}, }, { ObjectMeta: model.ObjectMeta{ Name: "instance2", }, - Spec: &model.InstanceSpec{ - Name: "instance2", - }, + Spec: &model.InstanceSpec{}, Status: model.InstanceStatus{}, }} case "/federation/registry": @@ -228,18 +224,14 @@ func InitializeMockSymphonyAPI() *httptest.Server { ObjectMeta: model.ObjectMeta{ Name: "catalog1", }, - Spec: &model.CatalogSpec{ - Name: "catalog1", - }, + Spec: &model.CatalogSpec{}, Status: &model.CatalogStatus{}, }, { ObjectMeta: model.ObjectMeta{ Name: "catalog2", }, - Spec: &model.CatalogSpec{ - Name: "catalog2", - }, + Spec: &model.CatalogSpec{}, Status: &model.CatalogStatus{}, }} default: diff --git a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go index 47bbf7f6a..b015de2ce 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go +++ b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go @@ -135,11 +135,11 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte creationCount := 0 for _, catalog := range catalogs { for _, object := range prefixedNames { - if catalog.Spec.Name == object { + if catalog.ObjectMeta.Name == object { objectData, _ := json.Marshal(catalog.Spec.Properties) //TODO: handle errors - name := catalog.Spec.Name + name := catalog.ObjectMeta.Name if s, ok := inputs["__origin"]; ok { - name = strings.TrimPrefix(catalog.Spec.Name, fmt.Sprintf("%s-", s)) + name = strings.TrimPrefix(catalog.ObjectMeta.Name, fmt.Sprintf("%s-", s)) } switch catalog.Spec.Type { case "instance": @@ -210,16 +210,12 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Errorf("Failed to unmarshal catalog state for catalog %s: %s", name, err.Error()) return outputs, false, err } - // If inner catalog defines a name, use it as the name - if catalogState.Spec.Name != "" { - catalogState.ObjectMeta.Name = catalogState.Spec.Name - } catalogState.ObjectMeta = updateObjectMeta(catalogState.ObjectMeta, inputs, name) objectData, _ := json.Marshal(catalogState) mLog.Debugf(" P (Materialize Processor): materialize catalog %v to namespace %s", catalogState.ObjectMeta.Name, catalogState.ObjectMeta.Namespace) - err = utils.UpsertCatalog(ctx, i.Config.BaseUrl, catalogState.Spec.Name, i.Config.User, i.Config.Password, objectData) + err = utils.UpsertCatalog(ctx, i.Config.BaseUrl, catalogState.ObjectMeta.Name, i.Config.User, i.Config.Password, objectData) if err != nil { - mLog.Errorf("Failed to create catalog %s: %s", catalogState.Spec.Name, err.Error()) + mLog.Errorf("Failed to create catalog %s: %s", catalogState.ObjectMeta.Name, err.Error()) return outputs, false, err } creationCount++ diff --git a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize_test.go b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize_test.go index 4c3b7bb88..b3fa5afe6 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize_test.go +++ b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize_test.go @@ -182,7 +182,6 @@ func InitializeMockSymphonyAPI(t *testing.T, expectNs string) *httptest.Server { }, Spec: &model.CatalogSpec{ Type: "target", - Name: "hq-target1", Properties: map[string]interface{}{ "spec": &model.TargetSpec{ DisplayName: "target1", @@ -199,13 +198,11 @@ func InitializeMockSymphonyAPI(t *testing.T, expectNs string) *httptest.Server { }, Spec: &model.CatalogSpec{ Type: "instance", - Name: "hq-instance1", Properties: map[string]interface{}{ - "spec": model.InstanceSpec{ - Name: "instance1", - }, + "spec": model.InstanceSpec{}, "metadata": &model.ObjectMeta{ Namespace: "objNS", + Name: "instance1", }, }, }, @@ -216,7 +213,6 @@ func InitializeMockSymphonyAPI(t *testing.T, expectNs string) *httptest.Server { }, Spec: &model.CatalogSpec{ Type: "solution", - Name: "hq-solution1", Properties: map[string]interface{}{ "spec": model.SolutionSpec{ DisplayName: "solution1", @@ -233,15 +229,14 @@ func InitializeMockSymphonyAPI(t *testing.T, expectNs string) *httptest.Server { }, Spec: &model.CatalogSpec{ Type: "catalog", - Name: "hq-catalog1", Properties: map[string]interface{}{ "spec": model.CatalogSpec{ - Name: "catalog1", Type: "config", Properties: map[string]interface{}{}, }, "metadata": &model.ObjectMeta{ Namespace: "objNS", + Name: "catalog1", }, }, }, diff --git a/api/pkg/apis/v1alpha1/providers/stage/patch/patch_test.go b/api/pkg/apis/v1alpha1/providers/stage/patch/patch_test.go index 20afa4f67..16784a21c 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/patch/patch_test.go +++ b/api/pkg/apis/v1alpha1/providers/stage/patch/patch_test.go @@ -319,7 +319,6 @@ func InitializeMockSymphonyAPI() *httptest.Server { }, Spec: &model.CatalogSpec{ Type: "config", - Name: "catalog1", Properties: map[string]interface{}{ "testkey": "0", "testdict": []string{"1"}, diff --git a/api/pkg/apis/v1alpha1/providers/stage/wait/wait.go b/api/pkg/apis/v1alpha1/providers/stage/wait/wait.go index 2575be45c..1c4ebeab0 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/wait/wait.go +++ b/api/pkg/apis/v1alpha1/providers/stage/wait/wait.go @@ -181,7 +181,7 @@ func (i *WaitStageProvider) Process(ctx context.Context, mgrContext contexts.Man } for _, instance := range instances { for _, object := range prefixedNames { - if instance.Spec.Name == object { + if instance.ObjectMeta.Name == object { foundCount++ } } @@ -209,7 +209,7 @@ func (i *WaitStageProvider) Process(ctx context.Context, mgrContext contexts.Man } for _, catalog := range catalogs { for _, object := range prefixedNames { - if catalog.Spec.Name == object { + if catalog.ObjectMeta.Name == object { foundCount++ } } diff --git a/api/pkg/apis/v1alpha1/providers/stage/wait/wait_test.go b/api/pkg/apis/v1alpha1/providers/stage/wait/wait_test.go index 5cea32f3f..f805507ff 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/wait/wait_test.go +++ b/api/pkg/apis/v1alpha1/providers/stage/wait/wait_test.go @@ -176,11 +176,9 @@ func InitializeMockSymphonyAPI() *httptest.Server { case "/instances": response = []model.InstanceState{{ ObjectMeta: model.ObjectMeta{ - Name: "instance1", - }, - Spec: &model.InstanceSpec{ Name: "hq-instance1", }, + Spec: &model.InstanceSpec{}, Status: model.InstanceStatus{}, }} case "/federation/registry": @@ -194,11 +192,9 @@ func InitializeMockSymphonyAPI() *httptest.Server { case "/catalogs/registry": response = []model.CatalogState{{ ObjectMeta: model.ObjectMeta{ - Name: "catalog1", - }, - Spec: &model.CatalogSpec{ Name: "hq-catalog1", }, + Spec: &model.CatalogSpec{}, }} default: response = AuthResponse{ diff --git a/api/pkg/apis/v1alpha1/providers/states/k8s/k8s_test.go b/api/pkg/apis/v1alpha1/providers/states/k8s/k8s_test.go index 5cce8bef0..643c7e18d 100644 --- a/api/pkg/apis/v1alpha1/providers/states/k8s/k8s_test.go +++ b/api/pkg/apis/v1alpha1/providers/states/k8s/k8s_test.go @@ -99,7 +99,6 @@ func TestActivationUpsert(t *testing.T) { }, "spec": model.ActivationSpec{ Campaign: "c1", - Name: "a1", Stage: "s1", }, }, @@ -134,7 +133,6 @@ func TestActivationList(t *testing.T) { ID: "a1", Body: model.ActivationSpec{ Campaign: "c1", - Name: "a1", Stage: "s1", }, }, @@ -236,7 +234,6 @@ func TestActivationGet(t *testing.T) { }, "spec": model.ActivationSpec{ Campaign: "c1", - Name: "a1", Stage: "s1", }, }, @@ -295,7 +292,6 @@ func TestActivationUpsertWithState(t *testing.T) { }, "spec": model.ActivationSpec{ Campaign: "c1", - Name: "a1", Stage: "s1", }, "status": dict, diff --git a/api/pkg/apis/v1alpha1/providers/target/adb/adb.go b/api/pkg/apis/v1alpha1/providers/target/adb/adb.go index 0823ecb64..0f2b5700d 100644 --- a/api/pkg/apis/v1alpha1/providers/target/adb/adb.go +++ b/api/pkg/apis/v1alpha1/providers/target/adb/adb.go @@ -94,7 +94,7 @@ func (i *AdbProvider) Get(ctx context.Context, deployment model.DeploymentSpec, aLog.Errorf(" P (Android ADB): failed to get deployment, error: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) return nil, err } - aLog.Infof(" P (Android ADB): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + aLog.Infof(" P (Android ADB): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) ret := make([]model.ComponentSpec, 0) @@ -137,7 +137,7 @@ func (i *AdbProvider) Apply(ctx context.Context, deployment model.DeploymentSpec var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - aLog.Infof(" P (Android ADB Provider): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + aLog.Infof(" P (Android ADB Provider): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) components := step.GetComponents() diff --git a/api/pkg/apis/v1alpha1/providers/target/azure/adu/adu.go b/api/pkg/apis/v1alpha1/providers/target/azure/adu/adu.go index 318bcae97..9dcca74c0 100644 --- a/api/pkg/apis/v1alpha1/providers/target/azure/adu/adu.go +++ b/api/pkg/apis/v1alpha1/providers/target/azure/adu/adu.go @@ -138,7 +138,7 @@ func (i *ADUTargetProvider) Get(ctx context.Context, dep model.DeploymentSpec, r return nil, err } - sLog.Infof(" P (ADU Target): getting components: %s - %s, traceId: %s", dep.Instance.Spec.Scope, dep.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (ADU Target): getting components: %s - %s, traceId: %s", dep.Instance.Spec.Scope, dep.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) deployment, err := i.getDeployment() if err != nil { sLog.Errorf(" P (ADU Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) @@ -193,7 +193,7 @@ func (i *ADUTargetProvider) Apply(ctx context.Context, deployment model.Deployme var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (ADU Target): applying components: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (ADU Target): applying components: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) components := step.GetComponents() err = i.GetValidationRule(ctx).Validate(components) diff --git a/api/pkg/apis/v1alpha1/providers/target/azure/iotedge/iotedge.go b/api/pkg/apis/v1alpha1/providers/target/azure/iotedge/iotedge.go index fbdab7169..cfcaae885 100644 --- a/api/pkg/apis/v1alpha1/providers/target/azure/iotedge/iotedge.go +++ b/api/pkg/apis/v1alpha1/providers/target/azure/iotedge/iotedge.go @@ -185,7 +185,7 @@ func (i *IoTEdgeTargetProvider) Get(ctx context.Context, deployment model.Deploy var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (IoT Edge Target): getting components: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (IoT Edge Target): getting components: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) hubTwin, err := i.getIoTEdgeModuleTwin(ctx, "$edgeHub") if err != nil { @@ -208,7 +208,7 @@ func (i *IoTEdgeTargetProvider) Get(ctx context.Context, deployment model.Deploy return nil, err } var component model.ComponentSpec - component, err = toComponent(hubTwin, twin, deployment.Instance.Spec.Name, m) + component, err = toComponent(hubTwin, twin, deployment.Instance.ObjectMeta.Name, m) if err != nil { sLog.Errorf(" P (IoT Edge Target):failed to parse %s twin to component %+v, traceId: %s", k, err, span.SpanContext().TraceID().String()) return nil, err @@ -256,7 +256,7 @@ func (i *IoTEdgeTargetProvider) Apply(ctx context.Context, deployment model.Depl //updated modules := make(map[string]Module) for _, a := range components { - module, e := toModule(a, deployment.Instance.Spec.Name, deployment.Instance.Spec.Metadata[ENV_NAME], step.Target) + module, e := toModule(a, deployment.Instance.ObjectMeta.Name, deployment.Instance.Spec.Metadata[ENV_NAME], step.Target) if e != nil { ret[a.Name] = model.ComponentResultSpec{ Status: v1alpha2.UpdateFailed, @@ -269,7 +269,7 @@ func (i *IoTEdgeTargetProvider) Apply(ctx context.Context, deployment model.Depl modules[a.Name] = module } if len(modules) > 0 { - err = i.deployToIoTEdge(ctx, deployment.Instance.Spec.Name, deployment.Instance.Spec.Metadata, modules, edgeAgent, edgeHub) + err = i.deployToIoTEdge(ctx, deployment.Instance.ObjectMeta.Name, deployment.Instance.Spec.Metadata, modules, edgeAgent, edgeHub) if err != nil { sLog.Errorf(" P (IoT Edge Target): failed to deploy to IoT edge: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) return ret, err @@ -279,7 +279,7 @@ func (i *IoTEdgeTargetProvider) Apply(ctx context.Context, deployment model.Depl //delete modules = make(map[string]Module) for _, a := range components { - module, e := toModule(a, deployment.Instance.Spec.Name, deployment.Instance.Spec.Metadata[ENV_NAME], step.Target) + module, e := toModule(a, deployment.Instance.ObjectMeta.Name, deployment.Instance.Spec.Metadata[ENV_NAME], step.Target) if e != nil { ret[a.Name] = model.ComponentResultSpec{ Status: v1alpha2.DeleteFailed, @@ -291,7 +291,7 @@ func (i *IoTEdgeTargetProvider) Apply(ctx context.Context, deployment model.Depl modules[a.Name] = module } if len(modules) > 0 { - err = i.remvoefromIoTEdge(ctx, deployment.Instance.Spec.Name, deployment.Instance.Spec.Metadata, modules, edgeAgent, edgeHub) + err = i.remvoefromIoTEdge(ctx, deployment.Instance.ObjectMeta.Name, deployment.Instance.Spec.Metadata, modules, edgeAgent, edgeHub) if err != nil { sLog.Errorf(" P (IoT Edge Target): failed to remove from IoT edge: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) return ret, err diff --git a/api/pkg/apis/v1alpha1/providers/target/configmap/configmap.go b/api/pkg/apis/v1alpha1/providers/target/configmap/configmap.go index de0525525..cd5e4df96 100644 --- a/api/pkg/apis/v1alpha1/providers/target/configmap/configmap.go +++ b/api/pkg/apis/v1alpha1/providers/target/configmap/configmap.go @@ -208,7 +208,7 @@ func (i *ConfigMapTargetProvider) Get(ctx context.Context, deployment model.Depl ) var err error = nil defer utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (ConfigMap Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (ConfigMap Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) ret := make([]model.ComponentSpec, 0) for _, component := range references { @@ -250,7 +250,7 @@ func (i *ConfigMapTargetProvider) Apply(ctx context.Context, deployment model.De var err error = nil defer utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (ConfigMap Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (ConfigMap Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) components := step.GetComponents() err = i.GetValidationRule(ctx).Validate(components) diff --git a/api/pkg/apis/v1alpha1/providers/target/configmap/configmap_test.go b/api/pkg/apis/v1alpha1/providers/target/configmap/configmap_test.go index ea2f3df2b..dd57be89e 100644 --- a/api/pkg/apis/v1alpha1/providers/target/configmap/configmap_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/configmap/configmap_test.go @@ -162,9 +162,9 @@ func TestConfigMapTargetProviderApply(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "configs", + Name: "config-test", }, Spec: &model.InstanceSpec{ - Name: "config-test", Scope: "configs", }, }, @@ -209,9 +209,9 @@ func TestConfigMapTargetProviderGet(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "configs", + Name: "config-test", }, Spec: &model.InstanceSpec{ - Name: "config-test", Scope: "configs", }, }, @@ -268,9 +268,9 @@ func TestConfigMapTargetProviderDelete(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "configs", + Name: "config-test", }, Spec: &model.InstanceSpec{ - Name: "config-test", Scope: "configs", }, }, @@ -319,9 +319,9 @@ func TestConfigMapTargetProviderApplyGetDelete(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "configs", + Name: "config-test", }, Spec: &model.InstanceSpec{ - Name: "config-test", Scope: "configs", }, }, diff --git a/api/pkg/apis/v1alpha1/providers/target/docker/docker.go b/api/pkg/apis/v1alpha1/providers/target/docker/docker.go index 63bc33d78..da66f0153 100644 --- a/api/pkg/apis/v1alpha1/providers/target/docker/docker.go +++ b/api/pkg/apis/v1alpha1/providers/target/docker/docker.go @@ -93,7 +93,7 @@ func (i *DockerTargetProvider) Get(ctx context.Context, deployment model.Deploym var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Docker Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Docker Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { @@ -172,10 +172,10 @@ func (i *DockerTargetProvider) Apply(ctx context.Context, deployment model.Deplo var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Docker Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Docker Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) injections := &model.ValueInjections{ - InstanceId: deployment.Instance.Spec.Name, + InstanceId: deployment.Instance.ObjectMeta.Name, SolutionId: deployment.Instance.Spec.Solution, TargetId: deployment.ActiveTarget, } diff --git a/api/pkg/apis/v1alpha1/providers/target/helm/helm.go b/api/pkg/apis/v1alpha1/providers/target/helm/helm.go index ded5c3839..19d475279 100644 --- a/api/pkg/apis/v1alpha1/providers/target/helm/helm.go +++ b/api/pkg/apis/v1alpha1/providers/target/helm/helm.go @@ -283,7 +283,7 @@ func (i *HelmTargetProvider) Get(ctx context.Context, deployment model.Deploymen var err error var actionConfig *action.Configuration defer utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Helm Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Helm Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) actionConfig, err = i.createActionConfig(ctx, deployment.Instance.Spec.Scope) if err != nil { sLog.Error(err) @@ -392,7 +392,7 @@ func (i *HelmTargetProvider) Apply(ctx context.Context, deployment model.Deploym ) var err error defer utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Helm Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Helm Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) functionName := utils.GetFunctionName() applyTime := time.Now().UTC() @@ -499,7 +499,7 @@ func (i *HelmTargetProvider) Apply(ctx context.Context, deployment model.Deploym chart.Metadata.Tags = "SYM:" + helmProp.Chart.Repo //this is not used by Helm SDK, we use this to carry repo info postRender := &PostRenderer{ - instance: *deployment.Instance.Spec, + instance: deployment.Instance, populator: i.MetaPopulator, } installClient := configureInstallClient(component.Component.Name, &helmProp.Chart, &deployment, actionConfig, postRender) diff --git a/api/pkg/apis/v1alpha1/providers/target/helm/helm_test.go b/api/pkg/apis/v1alpha1/providers/target/helm/helm_test.go index f231307a4..098d90667 100644 --- a/api/pkg/apis/v1alpha1/providers/target/helm/helm_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/helm/helm_test.go @@ -164,9 +164,11 @@ func TestHelmTargetProviderInstall(t *testing.T) { }, }, Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "test-instance", + }, Spec: &model.InstanceSpec{ Scope: defaultTestScope, - Name: "test-instance", }, }, } @@ -198,9 +200,11 @@ func TestHelmTargetProviderGet(t *testing.T) { assert.Nil(t, err) components, err := provider.Get(context.Background(), model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "test-instance", + }, Spec: &model.InstanceSpec{ Scope: defaultTestScope, - Name: "test-instance", }, }, Solution: model.SolutionState{ @@ -319,9 +323,11 @@ func TestHelmTargetProvider_NonOciChart(t *testing.T) { }, }, Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "test-instance-no-oci", + }, Spec: &model.InstanceSpec{ Scope: defaultTestScope, - Name: "test-instance-no-oci", }, }, } @@ -444,9 +450,11 @@ func TestHelmTargetProviderInstallDirectDownload(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "test-instance", + }, Spec: &model.InstanceSpec{ Scope: defaultTestScope, - Name: "test-instance", }, }, Solution: model.SolutionState{ @@ -490,9 +498,11 @@ func TestHelmTargetProviderRemove(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "test-instance", + }, Spec: &model.InstanceSpec{ Scope: defaultTestScope, - Name: "test-instance", }, }, Solution: model.SolutionState{ diff --git a/api/pkg/apis/v1alpha1/providers/target/helm/postrenderer.go b/api/pkg/apis/v1alpha1/providers/target/helm/postrenderer.go index 2fd4ed488..c3dcaf905 100644 --- a/api/pkg/apis/v1alpha1/providers/target/helm/postrenderer.go +++ b/api/pkg/apis/v1alpha1/providers/target/helm/postrenderer.go @@ -19,7 +19,7 @@ import ( type ( PostRenderer struct { populator metahelper.MetaPopulator - instance model.InstanceSpec + instance model.InstanceState } encodable interface{ UnstructuredContent() map[string]interface{} } ) diff --git a/api/pkg/apis/v1alpha1/providers/target/http/http.go b/api/pkg/apis/v1alpha1/providers/target/http/http.go index a1f588816..91ffd6011 100644 --- a/api/pkg/apis/v1alpha1/providers/target/http/http.go +++ b/api/pkg/apis/v1alpha1/providers/target/http/http.go @@ -90,7 +90,7 @@ func (i *HttpTargetProvider) Get(ctx context.Context, deployment model.Deploymen var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (HTTP Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (HTTP Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) // This provider doesn't remember what it does, so it always return nil when asked return nil, nil @@ -103,10 +103,10 @@ func (i *HttpTargetProvider) Apply(ctx context.Context, deployment model.Deploym var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (HTTP Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (HTTP Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) injections := &model.ValueInjections{ - InstanceId: deployment.Instance.Spec.Name, + InstanceId: deployment.Instance.ObjectMeta.Name, SolutionId: deployment.Instance.Spec.Solution, TargetId: deployment.ActiveTarget, } diff --git a/api/pkg/apis/v1alpha1/providers/target/ingress/ingress.go b/api/pkg/apis/v1alpha1/providers/target/ingress/ingress.go index ce6435edf..5d23b4401 100644 --- a/api/pkg/apis/v1alpha1/providers/target/ingress/ingress.go +++ b/api/pkg/apis/v1alpha1/providers/target/ingress/ingress.go @@ -209,7 +209,7 @@ func (i *IngressTargetProvider) Get(ctx context.Context, deployment model.Deploy ) var err error defer utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Ingress Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Ingress Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) ret := make([]model.ComponentSpec, 0) for _, component := range references { @@ -243,7 +243,7 @@ func (i *IngressTargetProvider) Apply(ctx context.Context, deployment model.Depl ) var err error defer utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Ingress Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Ingress Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) components := step.GetComponents() err = i.GetValidationRule(ctx).Validate(components) diff --git a/api/pkg/apis/v1alpha1/providers/target/ingress/ingress_test.go b/api/pkg/apis/v1alpha1/providers/target/ingress/ingress_test.go index 4f801e44e..168800c89 100644 --- a/api/pkg/apis/v1alpha1/providers/target/ingress/ingress_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/ingress/ingress_test.go @@ -151,10 +151,9 @@ func TestIngressTargetProviderApply(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "ingresses", + Name: "test-ingress", }, - Spec: &model.InstanceSpec{ - Name: "test-ingress", - }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -222,10 +221,9 @@ func TestIngressTargetProviderDelete(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "ingresses", + Name: "test-ingress", }, - Spec: &model.InstanceSpec{ - Name: "test-ingress", - }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -268,10 +266,9 @@ func TestIngressTargetProviderGet(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "ingresses", + Name: "ingress-test", }, - Spec: &model.InstanceSpec{ - Name: "ingress-test", - }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -344,10 +341,9 @@ func TestIngressTargetProviderApplyGet(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "ingresses", + Name: "test-ingress", }, - Spec: &model.InstanceSpec{ - Name: "test-ingress", - }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ diff --git a/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go b/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go index b8c6c734f..776d81350 100644 --- a/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go +++ b/api/pkg/apis/v1alpha1/providers/target/k8s/k8s.go @@ -290,13 +290,13 @@ func (i *K8sTargetProvider) Get(ctx context.Context, dep model.DeploymentSpec, r }) var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - log.Infof(" P (K8s Target Provider): getting artifacts: %s - %s, traceId: %s", dep.Instance.Spec.Scope, dep.Instance.Spec.Name, span.SpanContext().TraceID().String()) + log.Infof(" P (K8s Target Provider): getting artifacts: %s - %s, traceId: %s", dep.Instance.Spec.Scope, dep.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) var components []model.ComponentSpec switch i.Config.DeploymentStrategy { case "", SINGLE_POD: - components, err = i.getDeployment(ctx, dep.Instance.Spec.Scope, dep.Instance.Spec.Name) + components, err = i.getDeployment(ctx, dep.Instance.Spec.Scope, dep.Instance.ObjectMeta.Name) if err != nil { log.Debugf(" P (K8s Target Provider): failed to get - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to get components from deployment spec", componentName), v1alpha2.GetComponentSpecFailed) @@ -306,7 +306,7 @@ func (i *K8sTargetProvider) Get(ctx context.Context, dep model.DeploymentSpec, r components = make([]model.ComponentSpec, 0) scope := dep.Instance.Spec.Scope if i.Config.DeploymentStrategy == SERVICES_NS { - scope = dep.Instance.Spec.Name + scope = dep.Instance.ObjectMeta.Name } slice := dep.GetComponentSlice() for _, component := range slice { @@ -651,7 +651,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - log.Infof(" P (K8s Target Provider): applying artifacts: %s - %s, traceId: %s", dep.Instance.Spec.Scope, dep.Instance.Spec.Name, span.SpanContext().TraceID().String()) + log.Infof(" P (K8s Target Provider): applying artifacts: %s - %s, traceId: %s", dep.Instance.Spec.Scope, dep.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) components := step.GetComponents() err = i.GetValidationRule(ctx).Validate(components) @@ -677,7 +677,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, case "", SINGLE_POD: updated := step.GetUpdatedComponents() if len(updated) > 0 { - err = i.deployComponents(ctx, span, dep.Instance.Spec.Scope, dep.Instance.Spec.Name, dep.Instance.Spec.Metadata, components, projector, dep.Instance.Spec.Name) + err = i.deployComponents(ctx, span, dep.Instance.Spec.Scope, dep.Instance.ObjectMeta.Name, dep.Instance.Spec.Metadata, components, projector, dep.Instance.ObjectMeta.Name) if err != nil { log.Debugf(" P (K8s Target Provider): failed to apply components: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to deploy components", componentName), v1alpha2.K8sDeploymentFailed) @@ -686,7 +686,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, } deleted := step.GetDeletedComponents() if len(deleted) > 0 { - serviceName := dep.Instance.Spec.Name + serviceName := dep.Instance.ObjectMeta.Name if v, ok := dep.Instance.Spec.Metadata["service.name"]; ok && v != "" { serviceName = v } @@ -696,7 +696,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to remove k8s service", componentName), v1alpha2.K8sRemoveServiceFailed) return ret, err } - err = i.removeDeployment(ctx, dep.Instance.Spec.Scope, dep.Instance.Spec.Name) + err = i.removeDeployment(ctx, dep.Instance.Spec.Scope, dep.Instance.ObjectMeta.Name) if err != nil { log.Debugf(" P (K8s Target Provider): failed to remove deployment: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to remove k8s deployment", componentName), v1alpha2.K8sRemoveDeploymentFailed) @@ -714,7 +714,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, if len(updated) > 0 { scope := dep.Instance.Spec.Scope if i.Config.DeploymentStrategy == SERVICES_NS { - scope = dep.Instance.Spec.Name + scope = dep.Instance.ObjectMeta.Name } for _, component := range components { if dep.Instance.Spec.Metadata != nil { @@ -725,7 +725,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, component.Metadata[ENV_NAME] = v } } - err = i.deployComponents(ctx, span, scope, component.Name, component.Metadata, []model.ComponentSpec{component}, projector, dep.Instance.Spec.Name) + err = i.deployComponents(ctx, span, scope, component.Name, component.Metadata, []model.ComponentSpec{component}, projector, dep.Instance.ObjectMeta.Name) if err != nil { log.Debugf(" P (K8s Target Provider): failed to apply components: %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to deploy components", componentName), v1alpha2.K8sDeploymentFailed) @@ -737,7 +737,7 @@ func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, if len(deleted) > 0 { scope := dep.Instance.Spec.Scope if i.Config.DeploymentStrategy == SERVICES_NS { - scope = dep.Instance.Spec.Name + scope = dep.Instance.ObjectMeta.Name } for _, component := range deleted { serviceName := component.Name diff --git a/api/pkg/apis/v1alpha1/providers/target/k8s/k8s_test.go b/api/pkg/apis/v1alpha1/providers/target/k8s/k8s_test.go index 77c1e9336..f56e27458 100644 --- a/api/pkg/apis/v1alpha1/providers/target/k8s/k8s_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/k8s/k8s_test.go @@ -333,10 +333,9 @@ func TestDeployment(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "default", + Name: "name", }, - Spec: &model.InstanceSpec{ - Name: "name", - }, + Spec: &model.InstanceSpec{}, }, } _, err = provider.Get(context.Background(), deployment, []model.ComponentStep{}) @@ -372,10 +371,9 @@ func TestDeployment(t *testing.T) { Instance: model.InstanceState{ ObjectMeta: model.ObjectMeta{ Namespace: "default", + Name: "name", }, - Spec: &model.InstanceSpec{ - Name: "name", - }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ diff --git a/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl.go b/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl.go index bb01c31da..65c08c6ff 100644 --- a/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl.go +++ b/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl.go @@ -272,7 +272,7 @@ func (i *KubectlTargetProvider) Get(ctx context.Context, deployment model.Deploy ) var err error defer utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Kubectl Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Kubectl Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) ret := make([]model.ComponentSpec, 0) for _, component := range references { @@ -361,7 +361,7 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl ) var err error defer utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Kubectl Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Kubectl Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) functionName := utils.GetFunctionName() applyTime := time.Now().UTC() @@ -414,7 +414,7 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl } i.ensureNamespace(ctx, deployment.Instance.Spec.Scope) - err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope, *deployment.Instance.Spec) + err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope, deployment.Instance) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to apply Yaml: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to apply Yaml", providerName), v1alpha2.ApplyYamlFailed) @@ -500,7 +500,7 @@ func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.Depl } i.ensureNamespace(ctx, deployment.Instance.Spec.Scope) - err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope, *deployment.Instance.Spec) + err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Spec.Scope, deployment.Instance) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to apply custom resource: %+v, traceId: %s", err, err, span.SpanContext().TraceID().String()) err = v1alpha2.NewCOAError(err, fmt.Sprintf("%s: failed to apply custom resource", providerName), v1alpha2.ApplyResourceFailed) @@ -1038,7 +1038,7 @@ func (i *KubectlTargetProvider) deleteCustomResource(ctx context.Context, dataBy } // applyCustomResource applies a custom resource from a byte array -func (i *KubectlTargetProvider) applyCustomResource(ctx context.Context, dataBytes []byte, namespace string, instance model.InstanceSpec) error { +func (i *KubectlTargetProvider) applyCustomResource(ctx context.Context, dataBytes []byte, namespace string, instance model.InstanceState) error { obj, dr, err := i.buildDynamicResourceClient(dataBytes, namespace) if err != nil { sLog.Errorf(" P (Kubectl Target): failed to build a new dynamic client: %+v", err) diff --git a/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl_test.go b/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl_test.go index 400fa9c8e..23f783e42 100644 --- a/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/kubectl/kubectl_test.go @@ -168,9 +168,11 @@ func TestKubectlTargetProviderPathApplyAndDelete(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "nginx", + }, Spec: &model.InstanceSpec{ Scope: "nginx-deployment", - Name: "nginx", }, }, Solution: model.SolutionState{ @@ -261,8 +263,10 @@ func TestKubectlTargetProviderInlineApply(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "nginx-deployment", + }, Spec: &model.InstanceSpec{ - Name: "nginx-deployment", Scope: "default", }, }, @@ -342,8 +346,10 @@ func TestKubectlTargetProviderInlineUpdate(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "test-instance-iu", + }, Spec: &model.InstanceSpec{ - Name: "test-instance-iu", Scope: "test-scope-iu", }, }, @@ -445,8 +451,10 @@ func TestKubectlTargetProviderInlineStatusProbeApply(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "test-instance-spa", + }, Spec: &model.InstanceSpec{ - Name: "test-instance-spa", Scope: "test-scope-spa", }, }, @@ -553,9 +561,10 @@ func TestKubectlTargetProviderClusterLevelInlineApply(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "gatekeeper", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -600,9 +609,10 @@ func TestKubectlTargetProviderApplyPolicy(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "policies", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -680,9 +690,10 @@ func TestKubectlTargetProviderDeleteInline(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "nginx-deployment", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -727,9 +738,10 @@ func TestKubectlTargetProviderDeletePolicies(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "policies", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -799,9 +811,11 @@ func TestKubectlTargetProviderApplyFailed(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "nginx", + }, Spec: &model.InstanceSpec{ Scope: "nginx-system", - Name: "nginx", }, }, Solution: model.SolutionState{ @@ -862,9 +876,10 @@ func TestKubectlTargetProviderGet(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "policies", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -896,8 +911,10 @@ func TestKubectlTargetProviderGet(t *testing.T) { } deployment = model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "nginx", + }, Spec: &model.InstanceSpec{ - Name: "nginx", Scope: "nginx-system", }, }, diff --git a/api/pkg/apis/v1alpha1/providers/target/mock/mock_test.go b/api/pkg/apis/v1alpha1/providers/target/mock/mock_test.go index e7667c167..c532741f4 100644 --- a/api/pkg/apis/v1alpha1/providers/target/mock/mock_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/mock/mock_test.go @@ -37,9 +37,11 @@ func TestMockTargetProviderApply(t *testing.T) { deployment := model.DeploymentSpec{ Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "name", + }, Spec: &model.InstanceSpec{ Scope: "default", - Name: "name", }, }, } diff --git a/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt.go b/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt.go index 2b97485d7..5930e7518 100644 --- a/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt.go +++ b/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt.go @@ -235,7 +235,7 @@ func (i *MQTTTargetProvider) Get(ctx context.Context, deployment model.Deploymen }) var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (MQTT Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (MQTT Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) data, _ := json.Marshal(deployment) request := v1alpha2.COARequest{ @@ -291,7 +291,7 @@ func (i *MQTTTargetProvider) Remove(ctx context.Context, deployment model.Deploy var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (MQTT Target): deleting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (MQTT Target): deleting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) data, _ := json.Marshal(deployment) request := v1alpha2.COARequest{ @@ -335,7 +335,7 @@ func (i *MQTTTargetProvider) Apply(ctx context.Context, deployment model.Deploym var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (MQTT Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (MQTT Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) components := step.GetComponents() err = i.GetValidationRule(ctx).Validate(components) diff --git a/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt_test.go b/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt_test.go index 98c50678f..d35f300cf 100644 --- a/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/mqtt/mqtt_test.go @@ -314,8 +314,10 @@ func TestApply(t *testing.T) { SolutionName: "test-solution", Solution: model.SolutionState{}, Instance: model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "test-instance", + }, Spec: &model.InstanceSpec{ - Name: "test-instance", DisplayName: "test-instance", Solution: "test-solution", Target: model.TargetSelector{ diff --git a/api/pkg/apis/v1alpha1/providers/target/proxy/proxy.go b/api/pkg/apis/v1alpha1/providers/target/proxy/proxy.go index c138b84bc..b89eefb44 100644 --- a/api/pkg/apis/v1alpha1/providers/target/proxy/proxy.go +++ b/api/pkg/apis/v1alpha1/providers/target/proxy/proxy.go @@ -124,7 +124,7 @@ func (i *ProxyUpdateProvider) Get(ctx context.Context, deployment model.Deployme var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Proxy Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Proxy Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) data, _ := json.Marshal(deployment) payload, err := i.callRestAPI("instances", "GET", data) @@ -149,7 +149,7 @@ func (i *ProxyUpdateProvider) Apply(ctx context.Context, deployment model.Deploy var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Proxy Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Proxy Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) components := step.GetComponents() err = i.GetValidationRule(ctx).Validate(components) diff --git a/api/pkg/apis/v1alpha1/providers/target/script/script.go b/api/pkg/apis/v1alpha1/providers/target/script/script.go index 57a6e0d77..6ab9d9632 100644 --- a/api/pkg/apis/v1alpha1/providers/target/script/script.go +++ b/api/pkg/apis/v1alpha1/providers/target/script/script.go @@ -191,7 +191,7 @@ func (i *ScriptProvider) Get(ctx context.Context, deployment model.DeploymentSpe var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Script Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Script Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) id := uuid.New().String() input := id + ".json" @@ -313,7 +313,7 @@ func (i *ScriptProvider) Apply(ctx context.Context, deployment model.DeploymentS }) var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Script Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Script Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) functionName := observ_utils.GetFunctionName() err = i.GetValidationRule(ctx).Validate([]model.ComponentSpec{}) //this provider doesn't handle any components TODO: is this right? diff --git a/api/pkg/apis/v1alpha1/providers/target/staging/staging.go b/api/pkg/apis/v1alpha1/providers/target/staging/staging.go index 94dbcf16e..f6d2e6467 100644 --- a/api/pkg/apis/v1alpha1/providers/target/staging/staging.go +++ b/api/pkg/apis/v1alpha1/providers/target/staging/staging.go @@ -100,7 +100,7 @@ func (i *StagingTargetProvider) Get(ctx context.Context, deployment model.Deploy catalog, err := utils.GetCatalog( ctx, i.Context.SiteInfo.CurrentSite.BaseUrl, - deployment.Instance.Spec.Name+"-"+i.Config.TargetName, + deployment.Instance.ObjectMeta.Name+"-"+i.Config.TargetName, i.Context.SiteInfo.CurrentSite.Username, i.Context.SiteInfo.CurrentSite.Password, scope) @@ -165,7 +165,7 @@ func (i *StagingTargetProvider) Apply(ctx context.Context, deployment model.Depl catalog, err = utils.GetCatalog( ctx, i.Context.SiteInfo.CurrentSite.BaseUrl, - deployment.Instance.Spec.Name+"-"+i.Config.TargetName, + deployment.Instance.ObjectMeta.Name+"-"+i.Config.TargetName, i.Context.SiteInfo.CurrentSite.Username, i.Context.SiteInfo.CurrentSite.Password, scope) @@ -176,11 +176,10 @@ func (i *StagingTargetProvider) Apply(ctx context.Context, deployment model.Depl } if catalog.Spec == nil { - catalog.ObjectMeta.Name = deployment.Instance.Spec.Name + "-" + i.Config.TargetName + catalog.ObjectMeta.Name = deployment.Instance.ObjectMeta.Name + "-" + i.Config.TargetName catalog.Spec = &model.CatalogSpec{ SiteId: i.Context.SiteInfo.SiteId, Type: "staged", - Name: catalog.ObjectMeta.Name, } } if catalog.Spec.Properties == nil { @@ -274,7 +273,7 @@ func (i *StagingTargetProvider) Apply(ctx context.Context, deployment model.Depl err = utils.UpsertCatalog( ctx, i.Context.SiteInfo.CurrentSite.BaseUrl, - deployment.Instance.Spec.Name+"-"+i.Config.TargetName, + deployment.Instance.ObjectMeta.Name+"-"+i.Config.TargetName, i.Context.SiteInfo.CurrentSite.Username, i.Context.SiteInfo.CurrentSite.Password, jData) if err != nil { diff --git a/api/pkg/apis/v1alpha1/providers/target/staging/staging_test.go b/api/pkg/apis/v1alpha1/providers/target/staging/staging_test.go index 3e1da7f70..3e4edd305 100644 --- a/api/pkg/apis/v1alpha1/providers/target/staging/staging_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/staging/staging_test.go @@ -64,9 +64,10 @@ func TestStagingTargetProviderGet(t *testing.T) { assert.Nil(t, err) components, err := provider.Get(context.Background(), model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "test", }, + Spec: &model.InstanceSpec{}, }, }, []model.ComponentStep{ { @@ -116,9 +117,10 @@ func TestStagingTargetProviderApply(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "test", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ ObjectMeta: model.ObjectMeta{ @@ -175,9 +177,10 @@ func TestStagingTargetProviderRemove(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "test", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ ObjectMeta: model.ObjectMeta{ @@ -266,9 +269,10 @@ func TestApply(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "test", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ ObjectMeta: model.ObjectMeta{ @@ -364,9 +368,10 @@ func TestGet(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "test", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ ObjectMeta: model.ObjectMeta{ @@ -433,9 +438,10 @@ func TestGetCatalogsFailed(t *testing.T) { } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "test", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ ObjectMeta: model.ObjectMeta{ diff --git a/api/pkg/apis/v1alpha1/providers/target/win10/sideload/sideload.go b/api/pkg/apis/v1alpha1/providers/target/win10/sideload/sideload.go index 1fdae44f4..27604dedf 100644 --- a/api/pkg/apis/v1alpha1/providers/target/win10/sideload/sideload.go +++ b/api/pkg/apis/v1alpha1/providers/target/win10/sideload/sideload.go @@ -122,7 +122,7 @@ func (i *Win10SideLoadProvider) Get(ctx context.Context, deployment model.Deploy var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Win10Sideload Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Win10Sideload Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) params := make([]string, 0) params = append(params, "list") @@ -203,7 +203,7 @@ func (i *Win10SideLoadProvider) Apply(ctx context.Context, deployment model.Depl var err error = nil defer observ_utils.CloseSpanWithError(span, &err) - sLog.Infof(" P (Win10Sideload Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.Spec.Name, span.SpanContext().TraceID().String()) + sLog.Infof(" P (Win10Sideload Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) components := step.GetComponents() err = i.GetValidationRule(ctx).Validate(components) diff --git a/api/pkg/apis/v1alpha1/utils/metahelper/metahelper.go b/api/pkg/apis/v1alpha1/utils/metahelper/metahelper.go index a7124f7bc..cf7f6080a 100644 --- a/api/pkg/apis/v1alpha1/utils/metahelper/metahelper.go +++ b/api/pkg/apis/v1alpha1/utils/metahelper/metahelper.go @@ -15,11 +15,11 @@ import ( type ( MetaPopulator interface { - PopulateMeta(object metaV1.Object, instance model.InstanceSpec) error + PopulateMeta(object metaV1.Object, instance model.InstanceState) error } metaPopulator struct { - annotationPopulators []func(instance model.InstanceSpec) (map[string]string, error) - labelPopulators []func(instance model.InstanceSpec) (map[string]string, error) + annotationPopulators []func(instance model.InstanceState) (map[string]string, error) + labelPopulators []func(instance model.InstanceState) (map[string]string, error) } Option func(*metaPopulator) ) @@ -39,7 +39,7 @@ func WithDefaultPopulators() Option { } } -func (m *metaPopulator) PopulateMeta(object metaV1.Object, instance model.InstanceSpec) error { +func (m *metaPopulator) PopulateMeta(object metaV1.Object, instance model.InstanceState) error { if err := m.populateLabels(object, instance); err != nil { return err } @@ -49,7 +49,7 @@ func (m *metaPopulator) PopulateMeta(object metaV1.Object, instance model.Instan return nil } -func (m *metaPopulator) populateLabels(object metaV1.Object, instance model.InstanceSpec) error { +func (m *metaPopulator) populateLabels(object metaV1.Object, instance model.InstanceState) error { var labels []map[string]string labels = append(labels, object.GetLabels()) for _, f := range m.labelPopulators { @@ -63,7 +63,7 @@ func (m *metaPopulator) populateLabels(object metaV1.Object, instance model.Inst return nil } -func (m *metaPopulator) populateAnnotations(object metaV1.Object, instance model.InstanceSpec) error { +func (m *metaPopulator) populateAnnotations(object metaV1.Object, instance model.InstanceState) error { var annotations []map[string]string annotations = append(annotations, object.GetAnnotations()) for _, f := range m.annotationPopulators { @@ -77,14 +77,14 @@ func (m *metaPopulator) populateAnnotations(object metaV1.Object, instance model return nil } -func populateDefaultLabels(instance model.InstanceSpec) (map[string]string, error) { +func populateDefaultLabels(instance model.InstanceState) (map[string]string, error) { labels := make(map[string]string) labels[constants.ManagerMetaKey] = constants.API return labels, nil } -func populateDefaultAnnotations(instance model.InstanceSpec) (map[string]string, error) { +func populateDefaultAnnotations(instance model.InstanceState) (map[string]string, error) { annotations := make(map[string]string) - annotations[constants.InstanceMetaKey] = instance.Name + annotations[constants.InstanceMetaKey] = instance.ObjectMeta.Name return annotations, nil } diff --git a/api/pkg/apis/v1alpha1/utils/parser.go b/api/pkg/apis/v1alpha1/utils/parser.go index 141cb12e0..81075499e 100644 --- a/api/pkg/apis/v1alpha1/utils/parser.go +++ b/api/pkg/apis/v1alpha1/utils/parser.go @@ -794,7 +794,7 @@ func (n *FunctionNode) Eval(context utils.EvaluationContext) (interface{}, error case "instance": if len(n.Args) == 0 { if deploymentSpec, ok := context.DeploymentSpec.(model.DeploymentSpec); ok { - return deploymentSpec.Instance.Spec.Name, nil + return deploymentSpec.Instance.ObjectMeta.Name, nil } return nil, errors.New("deployment spec is not found") } diff --git a/api/pkg/apis/v1alpha1/utils/parser_test.go b/api/pkg/apis/v1alpha1/utils/parser_test.go index cb705e199..65432f64c 100644 --- a/api/pkg/apis/v1alpha1/utils/parser_test.go +++ b/api/pkg/apis/v1alpha1/utils/parser_test.go @@ -1252,9 +1252,10 @@ func TestConfigInExpression(t *testing.T) { parser := NewParser("[{\"name\":\"port${{$config(line-config-$instance(), SERVICE_PORT)}}\",\"port\": ${{$config(line-config-$instance(), SERVICE_PORT)}},\"targetPort\":5000}]") val, err := parser.Eval(utils.EvaluationContext{ConfigProvider: provider, DeploymentSpec: model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "instance1", }, + Spec: &model.InstanceSpec{}, }, }}) assert.Nil(t, err) @@ -1270,9 +1271,10 @@ func TestConfigObjectInExpression(t *testing.T) { parser := NewParser("${{$config('<' + 'line-config-' + $instance() + '>', \"\")}}") val, err := parser.Eval(utils.EvaluationContext{ConfigProvider: provider, DeploymentSpec: model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "instance1", }, + Spec: &model.InstanceSpec{}, }, }}) assert.Nil(t, err) @@ -1568,9 +1570,10 @@ func TestEvaulateInstance(t *testing.T) { val, err := parser.Eval(utils.EvaluationContext{ DeploymentSpec: model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "instance-1", }, + Spec: &model.InstanceSpec{}, }, SolutionName: "fake-solution", Solution: model.SolutionState{ diff --git a/api/pkg/apis/v1alpha1/utils/symphony-api.go b/api/pkg/apis/v1alpha1/utils/symphony-api.go index c5cee2c29..e10d19911 100644 --- a/api/pkg/apis/v1alpha1/utils/symphony-api.go +++ b/api/pkg/apis/v1alpha1/utils/symphony-api.go @@ -652,7 +652,6 @@ func CreateSymphonyDeploymentFromTarget(target model.TargetState, namespace stri }, Spec: &model.InstanceSpec{ Scope: scope, - Name: key, DisplayName: key, Solution: key, Target: model.TargetSelector{ @@ -702,9 +701,6 @@ func CreateSymphonyDeployment(instance model.InstanceState, solution model.Solut ret.Instance = instance ret.SolutionName = solution.ObjectMeta.Name ret.Instance.ObjectMeta.Name = instance.ObjectMeta.Name - if ret.Instance.Spec.Name == "" { - ret.Instance.Spec.Name = ret.Instance.ObjectMeta.Name - } assignments, err := AssignComponentsToTargets(ret.Solution.Spec.Components, ret.Targets) if err != nil { diff --git a/api/pkg/apis/v1alpha1/utils/symphony-api_test.go b/api/pkg/apis/v1alpha1/utils/symphony-api_test.go index ed01411aa..292352e36 100644 --- a/api/pkg/apis/v1alpha1/utils/symphony-api_test.go +++ b/api/pkg/apis/v1alpha1/utils/symphony-api_test.go @@ -596,7 +596,6 @@ func TestCreateSymphonyDeploymentFromTarget(t *testing.T) { }, Spec: &model.InstanceSpec{ Scope: "targetScope", - Name: "target-runtime-someTargetName", DisplayName: "target-runtime-someTargetName", Solution: "target-runtime-someTargetName", Target: model.TargetSelector{ @@ -758,7 +757,6 @@ func TestCreateSymphonyDeployment(t *testing.T) { Namespace: "instanceScope", }, Spec: &model.InstanceSpec{ - Name: "someOtherId", Solution: "", Scope: "default", // CreateSymphonyDeployment will give default if instance.Spec.Scope is empty Target: model.TargetSelector{ diff --git a/api/pkg/apis/v1alpha1/vendors/activations-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/activations-vendor_test.go index 2e95dc7c1..980c229e9 100644 --- a/api/pkg/apis/v1alpha1/vendors/activations-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/activations-vendor_test.go @@ -104,7 +104,6 @@ func TestActivationsOnActivations(t *testing.T) { assert.Equal(t, v1alpha2.InternalError, resp.State) activationState := model.ActivationState{ Spec: &model.ActivationSpec{ - Name: activationName, Campaign: campaignName, }, ObjectMeta: model.ObjectMeta{ diff --git a/api/pkg/apis/v1alpha1/vendors/campaigns-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/campaigns-vendor_test.go index f28950122..4e26a4cd2 100644 --- a/api/pkg/apis/v1alpha1/vendors/campaigns-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/campaigns-vendor_test.go @@ -69,9 +69,7 @@ func TestCampaignsInfo(t *testing.T) { } func TestCampaignsOnCampaigns(t *testing.T) { vendor := createCampaignsVendor() - campaignSpec := model.CampaignSpec{ - Name: "campaign1", - } + campaignSpec := model.CampaignSpec{} data, _ := json.Marshal(campaignSpec) resp := vendor.onCampaigns(v1alpha2.COARequest{ Method: fasthttp.MethodPost, @@ -118,9 +116,7 @@ func TestCampaignsOnCampaigns(t *testing.T) { } func TestCampaignsOnCampaignsFailure(t *testing.T) { vendor := createCampaignsVendor() - campaignSpec := model.CampaignSpec{ - Name: "campaign1", - } + campaignSpec := model.CampaignSpec{} data, _ := json.Marshal(campaignSpec) resp := vendor.onCampaigns(v1alpha2.COARequest{ Method: fasthttp.MethodGet, @@ -158,9 +154,7 @@ func TestCampaignsOnCampaignsFailure(t *testing.T) { func TestCampaignsWrongMethod(t *testing.T) { vendor := createCampaignsVendor() - campaignSpec := model.CampaignSpec{ - Name: "campaign1", - } + campaignSpec := model.CampaignSpec{} data, _ := json.Marshal(campaignSpec) resp := vendor.onCampaigns(v1alpha2.COARequest{ Method: fasthttp.MethodPut, diff --git a/api/pkg/apis/v1alpha1/vendors/catalogs-vendor.go b/api/pkg/apis/v1alpha1/vendors/catalogs-vendor.go index 5a3328798..b57ea3b2a 100644 --- a/api/pkg/apis/v1alpha1/vendors/catalogs-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/catalogs-vendor.go @@ -62,9 +62,8 @@ func (e *CatalogsVendor) Init(config vendors.VendorConfig, factories []managers. err = json.Unmarshal(jData, &catalog) origin := event.Metadata["origin"] if err == nil { - name := fmt.Sprintf("%s-%s", origin, catalog.Spec.Name) + name := fmt.Sprintf("%s-%s", origin, catalog.ObjectMeta.Name) catalog.ObjectMeta.Name = name - catalog.Spec.Name = name if catalog.Spec.ParentName != "" { catalog.Spec.ParentName = fmt.Sprintf("%s-%s", origin, catalog.Spec.ParentName) } diff --git a/api/pkg/apis/v1alpha1/vendors/catalogs-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/catalogs-vendor_test.go index 83efca678..22e5e806a 100644 --- a/api/pkg/apis/v1alpha1/vendors/catalogs-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/catalogs-vendor_test.go @@ -29,7 +29,6 @@ var catalogState = model.CatalogState{ }, Spec: &model.CatalogSpec{ SiteId: "site1", - Name: "name1", Type: "catalog", Properties: map[string]interface{}{ "property1": "value1", @@ -54,21 +53,19 @@ func CreateSimpleChain(root string, length int, CTManager catalogs.CatalogsManag json.Unmarshal(jData, &newCatalog) newCatalog.ObjectMeta.Name = root - newCatalog.Spec.Name = root newCatalog.Spec.ParentName = "" - err := CTManager.UpsertState(context.Background(), newCatalog.Spec.Name, newCatalog) + err := CTManager.UpsertState(context.Background(), newCatalog.ObjectMeta.Name, newCatalog) if err != nil { return err } for i := 1; i < length; i++ { - tmp := newCatalog.Spec.Name + tmp := newCatalog.ObjectMeta.Name var childCatalog model.CatalogState jData, _ := json.Marshal(newCatalog) json.Unmarshal(jData, &childCatalog) - childCatalog.Spec.Name = fmt.Sprintf("%s-%d", root, i) + childCatalog.ObjectMeta.Name = fmt.Sprintf("%s-%d", root, i) childCatalog.Spec.ParentName = tmp - childCatalog.ObjectMeta.Name = childCatalog.Spec.Name - err := CTManager.UpsertState(context.Background(), childCatalog.Spec.Name, childCatalog) + err := CTManager.UpsertState(context.Background(), childCatalog.ObjectMeta.Name, childCatalog) if err != nil { return err } @@ -86,10 +83,9 @@ func CreateSimpleBinaryTree(root string, depth int, CTManager catalogs.CatalogsM jData, _ := json.Marshal(catalog) json.Unmarshal(jData, &newCatalog) - newCatalog.Spec.Name = fmt.Sprintf("%s-%d", root, 0) - newCatalog.ObjectMeta.Name = newCatalog.Spec.Name + newCatalog.ObjectMeta.Name = fmt.Sprintf("%s-%d", root, 0) newCatalog.Spec.ParentName = "" - err := CTManager.UpsertState(context.Background(), newCatalog.Spec.Name, newCatalog) + err := CTManager.UpsertState(context.Background(), newCatalog.ObjectMeta.Name, newCatalog) if err != nil { return err } @@ -101,10 +97,9 @@ func CreateSimpleBinaryTree(root string, depth int, CTManager catalogs.CatalogsM var childCatalog model.CatalogState jData, _ := json.Marshal(newCatalog) json.Unmarshal(jData, &childCatalog) - childCatalog.Spec.Name = fmt.Sprintf("%s-%d", root, count) - childCatalog.ObjectMeta.Name = childCatalog.Spec.Name + childCatalog.ObjectMeta.Name = fmt.Sprintf("%s-%d", root, count) childCatalog.Spec.ParentName = fmt.Sprintf("%s-%d", root, parentIndex) - err := CTManager.UpsertState(context.Background(), childCatalog.Spec.Name, childCatalog) + err := CTManager.UpsertState(context.Background(), childCatalog.ObjectMeta.Name, childCatalog) if err != nil { return err } @@ -204,7 +199,9 @@ func TestCatalogOnCheck(t *testing.T) { response = vendor.onCheck(*requestPost) assert.Equal(t, v1alpha2.InternalError, response.State) - catalogState.Spec.Name = "test1" + catalogState.ObjectMeta = model.ObjectMeta{ + Name: "test1", + } catalogState.Spec.Metadata = map[string]string{ "schema": "EmailCheckSchema", } @@ -224,8 +221,7 @@ func TestCatalogOnCheck(t *testing.T) { catalogState.Spec.Properties = map[string]interface{}{ "spec": schema, } - catalogState.Spec.Name = "EmailCheckSchema" - catalogState.ObjectMeta.Name = catalogState.Spec.Name + catalogState.ObjectMeta.Name = "EmailCheckSchema" catalogState.Spec.ParentName = "" catalogState.Spec.Metadata = nil b, err = json.Marshal(catalogState) @@ -237,8 +233,7 @@ func TestCatalogOnCheck(t *testing.T) { response = vendor.onCatalogs(*requestPost) assert.Equal(t, v1alpha2.OK, response.State) - catalogState.Spec.Name = "test1" - catalogState.ObjectMeta.Name = catalogState.Spec.Name + catalogState.ObjectMeta.Name = "test1" catalogState.Spec.Metadata = map[string]string{ "schema": "EmailCheckSchema", } @@ -278,8 +273,7 @@ func TestCatalogOnCatalogsGet(t *testing.T) { response := vendor.onCatalogs(*requestGet) assert.Equal(t, v1alpha2.NotFound, response.State) - catalogState.Spec.Name = "test1" - catalogState.ObjectMeta.Name = catalogState.Spec.Name + catalogState.ObjectMeta.Name = "test1" b, err := json.Marshal(catalogState) assert.Nil(t, err) requestPost := &v1alpha2.COARequest{ @@ -300,7 +294,7 @@ func TestCatalogOnCatalogsGet(t *testing.T) { var summary model.CatalogState err = json.Unmarshal(response.Body, &summary) assert.Nil(t, err) - assert.Equal(t, catalogState.Spec.Name, summary.Spec.Name) + assert.Equal(t, catalogState.ObjectMeta.Name, summary.ObjectMeta.Name) requestGet.Parameters = nil response = vendor.onCatalogs(*requestGet) @@ -309,7 +303,7 @@ func TestCatalogOnCatalogsGet(t *testing.T) { err = json.Unmarshal(response.Body, &summarys) assert.Nil(t, err) assert.Equal(t, 1, len(summarys)) - assert.Equal(t, catalogState.Spec.Name, summarys[0].Spec.Name) + assert.Equal(t, catalogState.ObjectMeta.Name, summarys[0].ObjectMeta.Name) } func TestCatalogOnCatalogsPost(t *testing.T) { @@ -320,15 +314,14 @@ func TestCatalogOnCatalogsPost(t *testing.T) { Context: context.Background(), Body: []byte("wrongObject"), Parameters: map[string]string{ - "__name": catalogState.Spec.Name, + "__name": catalogState.ObjectMeta.Name, }, } response := vendor.onCatalogs(*requestPost) assert.Equal(t, v1alpha2.InternalError, response.State) - catalogState.Spec.Name = "test1" - catalogState.ObjectMeta.Name = catalogState.Spec.Name + catalogState.ObjectMeta.Name = "test1" b, err := json.Marshal(catalogState) assert.Nil(t, err) requestPost.Body = b @@ -354,7 +347,7 @@ func TestCatalogOnCatalogsPost(t *testing.T) { var summarys model.CatalogState err = json.Unmarshal(response.Body, &summarys) assert.Nil(t, err) - assert.Equal(t, catalogState.Spec.Name, summarys.Spec.Name) + assert.Equal(t, catalogState.ObjectMeta.Name, summarys.ObjectMeta.Name) } func TestCatalogOnCatalogsDelete(t *testing.T) { @@ -368,8 +361,7 @@ func TestCatalogOnCatalogsDelete(t *testing.T) { }, } - catalogState.Spec.Name = "test1" - catalogState.ObjectMeta.Name = catalogState.Spec.Name + catalogState.ObjectMeta.Name = "test1" b, err := json.Marshal(catalogState) assert.Nil(t, err) requestPost.Body = b @@ -511,7 +503,7 @@ func TestCatalogSubscribe(t *testing.T) { Method: fasthttp.MethodGet, Context: context.Background(), Parameters: map[string]string{ - "__name": fmt.Sprintf("%s-%s", origin, catalogState.Spec.Name), + "__name": fmt.Sprintf("%s-%s", origin, catalogState.ObjectMeta.Name), }, } response := vendor.onCatalogs(*requestGet) diff --git a/api/pkg/apis/v1alpha1/vendors/federation-vendor.go b/api/pkg/apis/v1alpha1/vendors/federation-vendor.go index 86fca689d..e45282cdb 100644 --- a/api/pkg/apis/v1alpha1/vendors/federation-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/federation-vendor.go @@ -421,7 +421,7 @@ func (f *FederationVendor) onK8sHook(request v1alpha2.COARequest) v1alpha2.COARe case fasthttp.MethodPost: objectType := request.Parameters["objectType"] if objectType == "catalog" { - var catalog model.CatalogSpec + var catalog model.CatalogState err := json.Unmarshal(request.Body, &catalog) if err != nil { return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ @@ -431,10 +431,10 @@ func (f *FederationVendor) onK8sHook(request v1alpha2.COARequest) v1alpha2.COARe } err = f.Vendor.Context.Publish("catalog", v1alpha2.Event{ Metadata: map[string]string{ - "objectType": catalog.Type, + "objectType": catalog.Spec.Type, }, Body: v1alpha2.JobData{ - Id: catalog.Name, + Id: catalog.ObjectMeta.Name, Action: v1alpha2.JobUpdate, //TODO: handle deletion, this probably requires BetBachForSites return flags Body: catalog, }, diff --git a/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go index 6a4ff0629..a8faf1949 100644 --- a/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go @@ -375,7 +375,6 @@ func TestFederationOnSyncGet(t *testing.T) { }, Spec: &model.CatalogSpec{ SiteId: vendor.Config.SiteInfo.SiteId, - Name: "catalog1", Type: "catalog", Properties: map[string]interface{}{ "property1": "value1", @@ -389,7 +388,7 @@ func TestFederationOnSyncGet(t *testing.T) { }, }, } - err = vendor.CatalogsManager.UpsertState(context.Background(), catalogState.Spec.Name, catalogState) + err = vendor.CatalogsManager.UpsertState(context.Background(), catalogState.ObjectMeta.Name, catalogState) assert.Nil(t, err) vendor.Context.PubsubProvider.Publish("catalog", v1alpha2.Event{ Metadata: map[string]string{ @@ -415,7 +414,7 @@ func TestFederationOnSyncGet(t *testing.T) { err = json.Unmarshal(response.Body, &summary) assert.Nil(t, err) if len(summary.Catalogs) == 1 { - assert.Equal(t, catalogState.Spec.Name, summary.Catalogs[0].Spec.Name) + assert.Equal(t, catalogState.ObjectMeta.Name, summary.Catalogs[0].ObjectMeta.Name) break } else { time.Sleep(time.Second) @@ -472,7 +471,6 @@ func TestFederationOnK8SHook(t *testing.T) { }, Spec: &model.CatalogSpec{ SiteId: vendor.Config.SiteInfo.SiteId, - Name: "catalog1", Type: "catalog", Properties: map[string]interface{}{ "property1": "value1", diff --git a/api/pkg/apis/v1alpha1/vendors/instances-vendor.go b/api/pkg/apis/v1alpha1/vendors/instances-vendor.go index 8aaec02b1..f3de309c0 100644 --- a/api/pkg/apis/v1alpha1/vendors/instances-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/instances-vendor.go @@ -138,7 +138,6 @@ func (c *InstancesVendor) onInstances(request v1alpha2.COARequest) v1alpha2.COAR }, Spec: &model.InstanceSpec{ DisplayName: id, - Name: id, Solution: solution, }, } @@ -173,9 +172,6 @@ func (c *InstancesVendor) onInstances(request v1alpha2.COARequest) v1alpha2.COAR if instance.ObjectMeta.Name == "" { instance.ObjectMeta.Name = id } - if instance.Spec.Name == "" { - instance.Spec.Name = id - } } err := c.InstancesManager.UpsertState(ctx, id, instance) if err != nil { diff --git a/api/pkg/apis/v1alpha1/vendors/solution-vendor.go b/api/pkg/apis/v1alpha1/vendors/solution-vendor.go index 8c181d678..c0fbccc7e 100644 --- a/api/pkg/apis/v1alpha1/vendors/solution-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/solution-vendor.go @@ -152,7 +152,7 @@ func (c *SolutionVendor) onQueue(request v1alpha2.COARequest) v1alpha2.COARespon Body: []byte(fmt.Sprintf(`{"result":"%s"}`, err.Error())), }) } - instance = deployment.Instance.Spec.Name + instance = deployment.Instance.ObjectMeta.Name } if instance == "" { diff --git a/api/pkg/apis/v1alpha1/vendors/solution-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/solution-vendor_test.go index e281661c0..9c3e994cf 100644 --- a/api/pkg/apis/v1alpha1/vendors/solution-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/solution-vendor_test.go @@ -80,9 +80,10 @@ func createSolutionVendor() SolutionVendor { func createDockerDeployment(id string) model.DeploymentSpec { return model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "instance-docker", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ @@ -123,9 +124,10 @@ func createDockerDeployment(id string) model.DeploymentSpec { func createDeployment2Mocks1Target(id string) model.DeploymentSpec { return model.DeploymentSpec{ Instance: model.InstanceState{ - Spec: &model.InstanceSpec{ + ObjectMeta: model.ObjectMeta{ Name: "instance1", }, + Spec: &model.InstanceSpec{}, }, Solution: model.SolutionState{ Spec: &model.SolutionSpec{ diff --git a/api/pkg/apis/v1alpha1/vendors/trails-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/trails-vendor_test.go index 47b770c57..b0e69230c 100644 --- a/api/pkg/apis/v1alpha1/vendors/trails-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/trails-vendor_test.go @@ -162,8 +162,11 @@ func TestTrailsVendorOnTrails_PostTrailsArrayAsBody(t *testing.T) { Catalog: "catalog1", Type: "solutions.solution.symphony/v1", Properties: map[string]interface{}{ - "post": model.CatalogSpec{ - Name: "test1", + "post": model.CatalogState{ + ObjectMeta: model.ObjectMeta{ + Name: "test1", + }, + Spec: &model.CatalogSpec{}, }, }, }, @@ -172,8 +175,11 @@ func TestTrailsVendorOnTrails_PostTrailsArrayAsBody(t *testing.T) { Catalog: "catalog2", Type: "solutions.solution.symphony/v1", Properties: map[string]interface{}{ - "post": model.CatalogSpec{ - Name: "test2", + "post": model.CatalogState{ + ObjectMeta: model.ObjectMeta{ + Name: "test2", + }, + Spec: &model.CatalogSpec{}, }, }, }, diff --git a/api/pkg/apis/v1alpha1/vendors/visualization-vendor.go b/api/pkg/apis/v1alpha1/vendors/visualization-vendor.go index 640c2ae43..b10bf5f59 100644 --- a/api/pkg/apis/v1alpha1/vendors/visualization-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/visualization-vendor.go @@ -155,7 +155,7 @@ func (c *VisualizationVendor) onVisPacket(request v1alpha2.COARequest) v1alpha2. } func (c *VisualizationVendor) updateSolutionTopologyCatalog(ctx context.Context, name string, catalog model.CatalogState) error { - catalog.Spec.Name = name + catalog.ObjectMeta.Name = name existingCatalog, err := c.CatalogsManager.GetState(ctx, name, catalog.ObjectMeta.Namespace) if err != nil { if !v1alpha2.IsNotFound(err) { diff --git a/docs/samples/approval/logicapp/activation.yaml b/docs/samples/approval/logicapp/activation.yaml index 0355992de..04accd781 100644 --- a/docs/samples/approval/logicapp/activation.yaml +++ b/docs/samples/approval/logicapp/activation.yaml @@ -3,5 +3,4 @@ kind: Activation metadata: name: approval-activation spec: - campaign: "approval-campaign" - name: "approval-activation" \ No newline at end of file + campaign: "approval-campaign" \ No newline at end of file diff --git a/docs/samples/approval/logicapp/instance-catalog.yaml b/docs/samples/approval/logicapp/instance-catalog.yaml index 0d9a5a7f2..d0f8bf856 100644 --- a/docs/samples/approval/logicapp/instance-catalog.yaml +++ b/docs/samples/approval/logicapp/instance-catalog.yaml @@ -5,10 +5,8 @@ metadata: spec: siteId: hq type: instance - name: gated-prometheus-instance properties: spec: - name: gated-prometheus-instance solution: gated-prometheus-server target: name: gated-k8s-target \ No newline at end of file diff --git a/docs/samples/approval/script/activation.yaml b/docs/samples/approval/script/activation.yaml index 0355992de..04accd781 100644 --- a/docs/samples/approval/script/activation.yaml +++ b/docs/samples/approval/script/activation.yaml @@ -3,5 +3,4 @@ kind: Activation metadata: name: approval-activation spec: - campaign: "approval-campaign" - name: "approval-activation" \ No newline at end of file + campaign: "approval-campaign" \ No newline at end of file diff --git a/docs/samples/approval/script/instance-catalog.yaml b/docs/samples/approval/script/instance-catalog.yaml index 0d9a5a7f2..d0f8bf856 100644 --- a/docs/samples/approval/script/instance-catalog.yaml +++ b/docs/samples/approval/script/instance-catalog.yaml @@ -5,10 +5,8 @@ metadata: spec: siteId: hq type: instance - name: gated-prometheus-instance properties: spec: - name: gated-prometheus-instance solution: gated-prometheus-server target: name: gated-k8s-target \ No newline at end of file diff --git a/docs/samples/campaigns/mock/activation.yaml b/docs/samples/campaigns/mock/activation.yaml index f0fd4b1f3..77e6c41c7 100644 --- a/docs/samples/campaigns/mock/activation.yaml +++ b/docs/samples/campaigns/mock/activation.yaml @@ -4,7 +4,6 @@ metadata: name: mock-activation spec: campaign: "mock-campaign" - name: "mock-activation" stage: "" inputs: foo: 0 \ No newline at end of file diff --git a/docs/samples/campaigns/remote-schedule/activation.yaml b/docs/samples/campaigns/remote-schedule/activation.yaml index 2f6732a26..f4e736228 100644 --- a/docs/samples/campaigns/remote-schedule/activation.yaml +++ b/docs/samples/campaigns/remote-schedule/activation.yaml @@ -3,5 +3,4 @@ kind: Activation metadata: name: scheduled-activation spec: - campaign: "scheduled-campaign" - name: "scheduled-activation" \ No newline at end of file + campaign: "scheduled-campaign" \ No newline at end of file diff --git a/docs/samples/campaigns/scheduled/activation.yaml b/docs/samples/campaigns/scheduled/activation.yaml index 2f6732a26..f4e736228 100644 --- a/docs/samples/campaigns/scheduled/activation.yaml +++ b/docs/samples/campaigns/scheduled/activation.yaml @@ -3,5 +3,4 @@ kind: Activation metadata: name: scheduled-activation spec: - campaign: "scheduled-campaign" - name: "scheduled-activation" \ No newline at end of file + campaign: "scheduled-campaign" \ No newline at end of file diff --git a/docs/samples/canary/activation.yaml b/docs/samples/canary/activation.yaml index 2edbff96f..6d1304d04 100644 --- a/docs/samples/canary/activation.yaml +++ b/docs/samples/canary/activation.yaml @@ -3,6 +3,4 @@ kind: Activation metadata: name: canary-deployment spec: - campaign: "canary" - name: "canary-deployment" - \ No newline at end of file + campaign: "canary" \ No newline at end of file diff --git a/docs/samples/multisite/activation.yaml b/docs/samples/multisite/activation.yaml index d3785a534..13ff05d5d 100644 --- a/docs/samples/multisite/activation.yaml +++ b/docs/samples/multisite/activation.yaml @@ -4,5 +4,4 @@ metadata: name: multisite-deploy spec: campaign: "site-apps" - name: "multisite-deploy" \ No newline at end of file diff --git a/docs/samples/multisite/catalog-catalog.yaml b/docs/samples/multisite/catalog-catalog.yaml index a84c11a60..472f02307 100644 --- a/docs/samples/multisite/catalog-catalog.yaml +++ b/docs/samples/multisite/catalog-catalog.yaml @@ -5,11 +5,11 @@ metadata: spec: siteId: hq type: catalog - name: site-catalog properties: + metadata: + name: web-app-config spec: type: config - name: web-app-config properties: image: "ghcr.io/eclipse-symphony/sample-flask-app:latest" serviceType: "LoadBalancer" \ No newline at end of file diff --git a/docs/samples/multisite/instance-catalog.yaml b/docs/samples/multisite/instance-catalog.yaml index c1edb119e..1e21206a2 100644 --- a/docs/samples/multisite/instance-catalog.yaml +++ b/docs/samples/multisite/instance-catalog.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: instance - name: site-instance properties: spec: solution: site-app diff --git a/docs/samples/multisite/solution-catalog.yaml b/docs/samples/multisite/solution-catalog.yaml index 883be9dea..940638c6f 100644 --- a/docs/samples/multisite/solution-catalog.yaml +++ b/docs/samples/multisite/solution-catalog.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: solution - name: site-app properties: spec: components: diff --git a/docs/samples/multisite/target-catalog.yaml b/docs/samples/multisite/target-catalog.yaml index 4ac1a7526..5a453d0f9 100644 --- a/docs/samples/multisite/target-catalog.yaml +++ b/docs/samples/multisite/target-catalog.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: target - name: site-k8s-target properties: spec: properties: diff --git a/docs/samples/piccolo/tiny_stack/activation.yaml b/docs/samples/piccolo/tiny_stack/activation.yaml index d3c825c42..f84036cbd 100644 --- a/docs/samples/piccolo/tiny_stack/activation.yaml +++ b/docs/samples/piccolo/tiny_stack/activation.yaml @@ -3,5 +3,4 @@ kind: Activation metadata: name: ebpf-approval spec: - campaign: ebpf-approval - name: ebpf-approval \ No newline at end of file + campaign: ebpf-approval \ No newline at end of file diff --git a/docs/samples/universe-data/chemical-factory-2/catalogs/assets.yaml b/docs/samples/universe-data/chemical-factory-2/catalogs/assets.yaml index c0b514553..6fe3e1761 100644 --- a/docs/samples/universe-data/chemical-factory-2/catalogs/assets.yaml +++ b/docs/samples/universe-data/chemical-factory-2/catalogs/assets.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: asset - name: hq properties: name: HQ address: 1 Microsoft Way @@ -25,7 +24,6 @@ metadata: spec: siteId: hq type: asset - name: infrastructure properties: name: "Infrastructure" parentName: hq @@ -37,7 +35,6 @@ metadata: spec: siteId: hq type: asset - name: l3 properties: name: "Level 3" parentName: infrastructure @@ -49,7 +46,6 @@ metadata: spec: siteId: hq type: asset - name: l4 properties: name: "Level 4" parentName: infrastructure @@ -61,7 +57,6 @@ metadata: spec: siteId: hq type: asset - name: use-cases properties: name: "Use Cases" parentName: hq @@ -73,7 +68,6 @@ metadata: spec: siteId: hq type: asset - name: csad properties: name: "CSAD" parentName: use-cases @@ -85,7 +79,6 @@ metadata: spec: siteId: hq type: asset - name: site properties: name: "Site" parentName: csad @@ -97,7 +90,6 @@ metadata: spec: siteId: hq type: asset - name: line-a properties: name: "Line A" parentName: site @@ -109,7 +101,6 @@ metadata: spec: siteId: hq type: asset - name: line-b properties: name: "Line B" parentName: site \ No newline at end of file diff --git a/docs/samples/universe-data/chemical-factory-2/catalogs/configurations.yaml b/docs/samples/universe-data/chemical-factory-2/catalogs/configurations.yaml index c67d24fbc..7117cff92 100644 --- a/docs/samples/universe-data/chemical-factory-2/catalogs/configurations.yaml +++ b/docs/samples/universe-data/chemical-factory-2/catalogs/configurations.yaml @@ -20,7 +20,6 @@ metadata: spec: siteId: hq type: config - name: l3-config metadata: asset: l3 properties: @@ -35,7 +34,6 @@ metadata: spec: siteId: hq type: config - name: l4-config metadata: asset: l4 properties: @@ -50,7 +48,6 @@ metadata: spec: siteId: hq type: config - name: csad-config parentName: global-config metadata: asset: use-case @@ -65,7 +62,6 @@ metadata: spec: siteId: hq type: config - name: site-config metadata: asset: site parentName: csad-config @@ -81,7 +77,6 @@ metadata: spec: siteId: hq type: config - name: line-a-config metadata: asset: line-a parentName: site-config @@ -96,7 +91,6 @@ metadata: spec: siteId: hq type: config - name: line-b-config metadata: asset: line-b parentName: site-config diff --git a/docs/samples/universe-data/chemical-factory/configurations-with-schema.yaml b/docs/samples/universe-data/chemical-factory/configurations-with-schema.yaml index f3a499ef5..1a855943a 100644 --- a/docs/samples/universe-data/chemical-factory/configurations-with-schema.yaml +++ b/docs/samples/universe-data/chemical-factory/configurations-with-schema.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: config - name: sample-config metadata: schema: sample-schema properties: diff --git a/docs/samples/universe-data/chemical-factory/configurations.yaml b/docs/samples/universe-data/chemical-factory/configurations.yaml index 8eca6732a..a74e897b2 100644 --- a/docs/samples/universe-data/chemical-factory/configurations.yaml +++ b/docs/samples/universe-data/chemical-factory/configurations.yaml @@ -22,7 +22,6 @@ metadata: spec: siteId: hq type: config - name: grafana-config properties: host: localhost port: 3000 @@ -37,7 +36,6 @@ metadata: spec: siteId: hq type: config - name: e4k-config properties: host: localhost port: 3000 @@ -52,7 +50,6 @@ metadata: spec: siteId: hq type: config - name: bluefin-config properties: host: localhost port: 3000 @@ -67,7 +64,6 @@ metadata: spec: siteId: hq type: config - name: ai-config metadata: asset: hq properties: @@ -82,7 +78,6 @@ metadata: spec: siteId: hq type: config - name: ai-config-site parentName: ai-config metadata: asset: hq-doe-site @@ -97,7 +92,6 @@ metadata: spec: siteId: hq type: config - name: ai-config-line metadata: asset: line-1 parentName: ai-config-site @@ -111,7 +105,6 @@ metadata: spec: siteId: hq type: config - name: combined properties: foo: bar ai: "" @@ -128,8 +121,7 @@ metadata: name: combined-1 spec: siteId: hq - type: config - name: combined-1 + type: config properties: foo: .foo loop: .loop @@ -141,7 +133,6 @@ metadata: spec: siteId: hq type: config - name: combined-2 properties: foo: bar2 loop: .loop @@ -153,7 +144,6 @@ metadata: spec: siteId: hq type: config - name: external properties: foo: far objectRef: diff --git a/docs/samples/universe-data/chemical-factory/manifests.yaml b/docs/samples/universe-data/chemical-factory/manifests.yaml index 16b922879..49f2e5337 100644 --- a/docs/samples/universe-data/chemical-factory/manifests.yaml +++ b/docs/samples/universe-data/chemical-factory/manifests.yaml @@ -17,7 +17,6 @@ metadata: spec: siteId: hq type: config - name: line-app-config properties: cat: leory dog: snoopy @@ -29,7 +28,6 @@ metadata: spec: siteId: hq type: solution - name: site-app properties: spec: displayName: site-app @@ -55,7 +53,6 @@ metadata: spec: siteId: hq type: solution - name: line-app properties: spec: components: @@ -143,7 +140,6 @@ metadata: spec: siteId: hq type: solution - name: smart-fridge properties: spec: displayName: smart-fridge @@ -171,7 +167,6 @@ metadata: spec: siteId: hq type: instance - name: site-instance properties: spec: solution: site-app diff --git a/docs/samples/universe-data/chemical-factory/schemas.yaml b/docs/samples/universe-data/chemical-factory/schemas.yaml index 10476f2eb..611a8d200 100644 --- a/docs/samples/universe-data/chemical-factory/schemas.yaml +++ b/docs/samples/universe-data/chemical-factory/schemas.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: schema - name: sample-schema properties: spec: rules: diff --git a/docs/samples/universe-data/chemical-factory/sites.yaml b/docs/samples/universe-data/chemical-factory/sites.yaml index df7a05641..6c46075e0 100644 --- a/docs/samples/universe-data/chemical-factory/sites.yaml +++ b/docs/samples/universe-data/chemical-factory/sites.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: asset - name: hq properties: name: HQ address: 1 Microsoft Way @@ -25,7 +24,6 @@ metadata: spec: siteId: tokyo type: asset - name: tokyo properties: name: "東京" address: "東京都港区港南 2-16-3" @@ -45,7 +43,6 @@ metadata: spec: siteId: new-york type: asset - name: new-york properties: name: "New York" address: "11 Times Square" @@ -65,7 +62,6 @@ metadata: spec: siteId: munchen type: asset - name: munchen properties: name: "München" address: "Walter-Gropius-Straße 5" @@ -85,7 +81,6 @@ metadata: spec: siteId: hq type: asset - name: hq-adr properties: name: "HQ Azure Device Registry" parentName: hq @@ -107,7 +102,6 @@ metadata: spec: siteId: hq type: asset - name: hq-arc-1 properties: name: "HQ Azure Arc Cluster 1" parentName: hq @@ -129,7 +123,6 @@ metadata: spec: siteId: hq type: asset - name: hq-arc-2 properties: name: "HQ Azure Arc Cluster 2" parentName: hq @@ -151,7 +144,6 @@ metadata: spec: siteId: hq type: asset - name: hq-doe-site properties: name: "HQ DOE Site" parentName: hq @@ -173,7 +165,6 @@ metadata: spec: siteId: hq type: asset - name: hq-iot-hub properties: name: "HQ IoT Hub Tenant" parentName: hq @@ -195,7 +186,6 @@ metadata: spec: siteId: hq type: asset - name: area-1 properties: name: "Area 1" parentName: hq-doe-site @@ -207,7 +197,6 @@ metadata: spec: siteId: hq type: asset - name: area-2 properties: name: "Area 2" parentName: hq-doe-site @@ -219,7 +208,6 @@ metadata: spec: siteId: hq type: asset - name: line-1 properties: name: "Production Line 1" parentName: area-1 @@ -231,7 +219,6 @@ metadata: spec: siteId: hq type: asset - name: line-2 properties: name: "Production Line 2" parentName: area-1 \ No newline at end of file diff --git a/docs/symphony-book/concepts/unified-object-model/catalog.md b/docs/symphony-book/concepts/unified-object-model/catalog.md index 447c0acfa..146155ad5 100644 --- a/docs/symphony-book/concepts/unified-object-model/catalog.md +++ b/docs/symphony-book/concepts/unified-object-model/catalog.md @@ -12,7 +12,6 @@ metadata: spec: siteId: hq type: asset - name: hq properties: name: HQ address: 1 Microsoft Way @@ -38,7 +37,6 @@ metadata: spec: siteId: hq type: edge - name: edges properties: node1: node2 node2: node3 @@ -56,7 +54,6 @@ metadata: spec: siteId: hq type: config - name: app-config parentName: global-config metadata: asset: use-case diff --git a/docs/symphony-book/configuration-management/define-configurations copy.md b/docs/symphony-book/configuration-management/define-configurations copy.md index 3aef63377..52344ea5e 100644 --- a/docs/symphony-book/configuration-management/define-configurations copy.md +++ b/docs/symphony-book/configuration-management/define-configurations copy.md @@ -10,7 +10,6 @@ metadata: spec: siteId: hq type: config - name: robot-config properties: name: my-robot os_version: "1.34" diff --git a/docs/symphony-book/configuration-management/define-configurations.md b/docs/symphony-book/configuration-management/define-configurations.md index 09e9f5681..b73710b17 100644 --- a/docs/symphony-book/configuration-management/define-configurations.md +++ b/docs/symphony-book/configuration-management/define-configurations.md @@ -10,7 +10,6 @@ metadata: spec: siteId: hq type: config - name: robot-config properties: name: my-robot os_version: "1.34" diff --git a/docs/symphony-book/configuration-management/late-assembly.md b/docs/symphony-book/configuration-management/late-assembly.md index e058539c9..0e5748010 100644 --- a/docs/symphony-book/configuration-management/late-assembly.md +++ b/docs/symphony-book/configuration-management/late-assembly.md @@ -35,7 +35,6 @@ metadata: spec: siteId: hq type: config - name: robot-config properties: some-object: "${{$json($config('', ''))}}" some-value: "${{$config('other-config','some-field)}}" diff --git a/k8s/apis/federation/v1/catalog_webhook.go b/k8s/apis/federation/v1/catalog_webhook.go index 78675d327..80e379d2e 100644 --- a/k8s/apis/federation/v1/catalog_webhook.go +++ b/k8s/apis/federation/v1/catalog_webhook.go @@ -28,7 +28,7 @@ var catalogWebhookValidationMetrics *metrics.Metrics func (r *Catalog) SetupWebhookWithManager(mgr ctrl.Manager) error { myCatalogClient = mgr.GetClient() - mgr.GetFieldIndexer().IndexField(context.Background(), &Catalog{}, ".spec.name", func(rawObj client.Object) []string { + mgr.GetFieldIndexer().IndexField(context.Background(), &Catalog{}, ".metadata.name", func(rawObj client.Object) []string { target := rawObj.(*Catalog) return []string{target.Name} }) @@ -127,7 +127,7 @@ func (r *Catalog) checkSchema() error { if schemaName, ok := r.Spec.Metadata["schema"]; ok { cataloglog.Info("Find schema name", "name", schemaName) var catalogs CatalogList - err := myCatalogClient.List(context.Background(), &catalogs, client.InNamespace(r.ObjectMeta.Namespace), client.MatchingFields{".spec.name": schemaName}) + err := myCatalogClient.List(context.Background(), &catalogs, client.InNamespace(r.ObjectMeta.Namespace), client.MatchingFields{".metadata.name": schemaName}) if err != nil || len(catalogs.Items) == 0 { cataloglog.Error(err, "Could not find the required schema.", "name", schemaName) return v1alpha2.NewCOAError(err, "schema not found", v1alpha2.NotFound) diff --git a/k8s/apis/model/v1/common_types.go b/k8s/apis/model/v1/common_types.go index c625a8c2a..f9d6cbe87 100644 --- a/k8s/apis/model/v1/common_types.go +++ b/k8s/apis/model/v1/common_types.go @@ -82,7 +82,6 @@ type TargetSpec struct { // +kubebuilder:object:generate=true type InstanceSpec struct { - Name string `json:"name"` DisplayName string `json:"displayName,omitempty"` Scope string `json:"scope,omitempty"` Parameters map[string]string `json:"parameters,omitempty"` //TODO: Do we still need this? @@ -135,7 +134,6 @@ type StageSpec struct { // +kubebuilder:object:generate=true type ActivationSpec struct { Campaign string `json:"campaign,omitempty"` - Name string `json:"name,omitempty"` Stage string `json:"stage,omitempty"` // +kubebuilder:pruning:PreserveUnknownFields // +kubebuilder:validation:Schemaless @@ -155,7 +153,6 @@ type CampaignSpec struct { type CatalogSpec struct { SiteId string `json:"siteId"` Type string `json:"type"` - Name string `json:"name"` Metadata map[string]string `json:"metadata,omitempty"` // +kubebuilder:pruning:PreserveUnknownFields // +kubebuilder:validation:Schemaless diff --git a/k8s/config/oss/crd/bases/federation.symphony_catalogs.yaml b/k8s/config/oss/crd/bases/federation.symphony_catalogs.yaml index 1133ee0ac..defdd5d53 100644 --- a/k8s/config/oss/crd/bases/federation.symphony_catalogs.yaml +++ b/k8s/config/oss/crd/bases/federation.symphony_catalogs.yaml @@ -40,8 +40,6 @@ spec: additionalProperties: type: string type: object - name: - type: string objectRef: properties: address: @@ -82,7 +80,6 @@ spec: type: type: string required: - - name - properties - siteId - type diff --git a/k8s/config/oss/crd/bases/solution.symphony_instances.yaml b/k8s/config/oss/crd/bases/solution.symphony_instances.yaml index b204c564b..537952e61 100644 --- a/k8s/config/oss/crd/bases/solution.symphony_instances.yaml +++ b/k8s/config/oss/crd/bases/solution.symphony_instances.yaml @@ -58,8 +58,6 @@ spec: additionalProperties: type: string type: object - name: - type: string parameters: additionalProperties: type: string @@ -139,7 +137,6 @@ spec: type: object type: array required: - - name - solution type: object status: diff --git a/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml b/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml index c737018cd..c1bf2e7e6 100644 --- a/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml +++ b/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml @@ -47,8 +47,6 @@ spec: type: string inputs: x-kubernetes-preserve-unknown-fields: true - name: - type: string stage: type: string type: object diff --git a/k8s/controllers/federation/catalog_controller.go b/k8s/controllers/federation/catalog_controller.go index 482fd9e2f..72e327c3f 100644 --- a/k8s/controllers/federation/catalog_controller.go +++ b/k8s/controllers/federation/catalog_controller.go @@ -48,7 +48,7 @@ func (r *CatalogReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } if catalog.ObjectMeta.DeletionTimestamp.IsZero() { // update - jData, _ := json.Marshal(catalog.Spec) + jData, _ := json.Marshal(catalog) err := api_utils.CatalogHook(ctx, "http://symphony-service:8080/v1alpha2/", "admin", "", jData) if err != nil { return ctrl.Result{}, err diff --git a/k8s/utils/symphony-api.go b/k8s/utils/symphony-api.go index 2ef63a2c3..2856b6973 100644 --- a/k8s/utils/symphony-api.go +++ b/k8s/utils/symphony-api.go @@ -109,7 +109,6 @@ func K8SInstanceToAPIInstanceState(instance solution_v1.Instance) (apimodel.Inst }, Spec: &apimodel.InstanceSpec{ Scope: instance.Spec.Scope, - Name: instance.Spec.Name, DisplayName: instance.Spec.DisplayName, Solution: instance.Spec.Solution, Target: instance.Spec.Target, diff --git a/packages/helm/symphony/templates/symphony.yaml b/packages/helm/symphony/templates/symphony.yaml index 34b0c0622..473f3fac4 100644 --- a/packages/helm/symphony/templates/symphony.yaml +++ b/packages/helm/symphony/templates/symphony.yaml @@ -57,8 +57,6 @@ spec: type: string inputs: x-kubernetes-preserve-unknown-fields: true - name: - type: string stage: type: string type: object @@ -232,8 +230,6 @@ spec: additionalProperties: type: string type: object - name: - type: string objectRef: properties: address: @@ -274,7 +270,6 @@ spec: type: type: string required: - - name - properties - siteId - type @@ -449,8 +444,6 @@ spec: additionalProperties: type: string type: object - name: - type: string parameters: additionalProperties: type: string @@ -530,7 +523,6 @@ spec: type: object type: array required: - - name - solution type: object status: diff --git a/test/integration/scenarios/04.workflow/manifest/activation.yaml b/test/integration/scenarios/04.workflow/manifest/activation.yaml index dc8093e5d..95c490e15 100644 --- a/test/integration/scenarios/04.workflow/manifest/activation.yaml +++ b/test/integration/scenarios/04.workflow/manifest/activation.yaml @@ -4,5 +4,4 @@ metadata: name: 04workflow spec: campaign: "04campaign" - name: "04workflow" \ No newline at end of file diff --git a/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml b/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml index bdd74c064..b0f42a19f 100644 --- a/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml +++ b/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: instance - name: site-instance properties: spec: solution: site-app diff --git a/test/integration/scenarios/05.catalog/catalogs/asset.yaml b/test/integration/scenarios/05.catalog/catalogs/asset.yaml index 5138d595f..0c35d65bb 100644 --- a/test/integration/scenarios/05.catalog/catalogs/asset.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/asset.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: asset - name: asset properties: name: "東京" address: "東京都港区港南 2-16-3" diff --git a/test/integration/scenarios/05.catalog/catalogs/config.yaml b/test/integration/scenarios/05.catalog/catalogs/config.yaml index 2f15b4dac..88b2899b8 100644 --- a/test/integration/scenarios/05.catalog/catalogs/config.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/config.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: config - name: config metadata: schema: schema properties: diff --git a/test/integration/scenarios/05.catalog/catalogs/instance.yaml b/test/integration/scenarios/05.catalog/catalogs/instance.yaml index 14459566b..69f942adb 100644 --- a/test/integration/scenarios/05.catalog/catalogs/instance.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/instance.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: instance - name: instance properties: spec: solution: app diff --git a/test/integration/scenarios/05.catalog/catalogs/schema.yaml b/test/integration/scenarios/05.catalog/catalogs/schema.yaml index 8d62992d2..dbcb85085 100644 --- a/test/integration/scenarios/05.catalog/catalogs/schema.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/schema.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: schema - name: schema properties: spec: rules: diff --git a/test/integration/scenarios/05.catalog/catalogs/solution.yaml b/test/integration/scenarios/05.catalog/catalogs/solution.yaml index f4c41a79a..a19bb7350 100644 --- a/test/integration/scenarios/05.catalog/catalogs/solution.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/solution.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: solution - name: solution properties: spec: displayName: site-app diff --git a/test/integration/scenarios/05.catalog/catalogs/target.yaml b/test/integration/scenarios/05.catalog/catalogs/target.yaml index bb7bf2755..a16994ef7 100644 --- a/test/integration/scenarios/05.catalog/catalogs/target.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/target.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: target - name: target properties: spec: properties: diff --git a/test/integration/scenarios/05.catalog/catalogs/wrongconfig.yaml b/test/integration/scenarios/05.catalog/catalogs/wrongconfig.yaml index 97952565c..3e3024d76 100644 --- a/test/integration/scenarios/05.catalog/catalogs/wrongconfig.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/wrongconfig.yaml @@ -5,7 +5,6 @@ metadata: spec: siteId: hq type: config - name: wrongconfig metadata: schema: schema properties: From f6b5ee2e9b30c8bea16b3aeb5836556d010aae8f Mon Sep 17 00:00:00 2001 From: Jesse Date: Wed, 8 May 2024 04:50:40 +0800 Subject: [PATCH 12/26] Adomerge packages (#237) * coa first round (#3) * coa first round * enable manual trigger * fix otel dependency issue * fix api and k8s tests * add header --------- Co-authored-by: Jiawei Du * API done * tidy api/go.mod * fixes * enable more cases * refine ensureNamespace * refine tests * tidy k8s/go.mod * refine cases * verify * fix mage remove * refine go.mod in api & k8s * refine go.work and go.mod in tests * fix naming * temp * enable ginkogo test * fix deploymenthash and tested ginkgo tests * fix github.com/stretchr/objx * fix startup * testing * fix requeue due to update change * resolve a rebase issue * [Temp] Enable tests * Refine mage commands for tests * fix tests and github action * remove adomerge_staging branch * increase go test timeout to 1min * increase go test timeout to 5min * fix constant value * fix unnecessary changes * refine gatekeeper tests * Merge ADO changes to OSS * refine k8s magefile * fix gatekeeper tests * export symphony logs in integration test * fix requeue when deployment is in-progress * Remove unnecessary isDeploymentFinished, use summaryResult.State to check * Remove unnecessary isDeploymentFinished, use summaryResult.State to check * fix merge issues * first draft * checkpoint SAT * second version * Merge ADO K8S changes to OSS * honor OSS changes * expose delete sync delay for futuer override from helm charts * generate helm charts * fix typo in InstanceStatus * add debug logs for k8s target provider * refactor webhook and add metrics * refactor reconciliation policy to meet API spec * watch operationId change instead of annnotation change * webhook fixes * fix typo * refine reconciliation policy * refactor doc * remove version property in target/intance since we will have new version implementation * fix typo * fix dependency issues * clean up work * add ado suite test pipeline * resolve comments * resolve comments * resolve comments * resolve comments * fix helmTemplate * change timeout to 90 in local env mage file * upate cert manager chart version --------- Co-authored-by: Jiawei Du Co-authored-by: Jiawei Du --- .github/workflows/suite.yml | 112 + api/Dockerfile | 2 +- api/symphony-script-over-mqtt.json | 2 +- coa/pkg/apis/v1alpha2/bindings/http/jwt.go | 11 + docs/symphony-book/build_deployment/build.md | 2 +- go.work.bk | 3 + go.work.sum | 2 +- .../oss/default/manager_webhook_patch.yaml | 5 + k8s/config/oss/helm/manager-patch.yaml | 25 +- k8s/magefile.go | 2 +- packages/go.work | 6 + packages/go.work.sum | 11 + packages/helm/symphony/Chart.yaml | 2 +- packages/helm/symphony/azure/metadata.json | 39 - .../helm/symphony/crds/cert-manager.crds.yaml | 5654 +++++++++-------- .../symphony/files/metric-middleware.json | 29 + .../helm/symphony/files/oss/delete-objects.sh | 3 + .../symphony/files/oss/remove-finalizers.sh | 5 +- .../helm/symphony/files/symphony-api.json | 52 +- .../helm/symphony/files/trace-middleware.json | 16 + .../templates/certificate/certificate.yaml | 15 + .../symphony/templates/env-configmap.yaml | 11 + .../{ => identity}/extension-identity.yaml | 2 +- .../hooks-cluster-role-binding.yaml | 0 .../{ => identity}/hooks-cluster-role.yaml | 0 .../{ => identity}/hooks-service-account.yaml | 0 .../pai-cluster-role-binding.yaml | 0 .../{ => identity}/pai-cluster-role.yaml | 0 .../{ => identity}/pai-service-account.yaml | 0 .../templates/outbound-proxy-secret.yaml | 29 + .../helm/symphony/templates/symphony-api.yaml | 52 - .../{ => symphony-core}/_helpers.tpl | 76 + .../{ => symphony-core}/delete-job.yaml | 0 .../remove-finializers-job.yaml | 0 .../templates/symphony-core/symphony-api.yaml | 110 + .../symphony-service-ext.yaml | 8 +- .../symphony-core/symphony-service.yaml | 18 + .../symphonyk8s.yaml} | 28 + .../symphony/templates/symphony-service.yaml | 18 - packages/helm/symphony/values.yaml | 21 +- packages/testutils/.gitignore | 1 + packages/testutils/.vscode/settings.json | 3 + packages/testutils/README.md | 136 + packages/testutils/conditions/README.md | 137 + packages/testutils/conditions/all.go | 90 + packages/testutils/conditions/any.go | 63 + packages/testutils/conditions/basic.go | 74 + .../testutils/conditions/condition_test.go | 310 + packages/testutils/conditions/count.go | 39 + packages/testutils/conditions/jq/README.md | 135 + packages/testutils/conditions/jq/common.go | 6 + packages/testutils/conditions/jq/jq.go | 167 + packages/testutils/conditions/jq/jq_test.go | 169 + .../testutils/conditions/jsonpath/README.md | 125 + .../testutils/conditions/jsonpath/jsonpath.go | 143 + .../conditions/jsonpath/jsonpath_test.go | 149 + packages/testutils/doc.go | 122 + packages/testutils/expectations/README.md | 127 + .../testutils/expectations/expectation.go | 126 + .../expectations/expectation_test.go | 228 + .../testutils/expectations/helm/README.md | 219 + .../testutils/expectations/helm/commons.go | 8 + .../testutils/expectations/helm/gomega.go | 32 + .../testutils/expectations/helm/options.go | 53 + .../testutils/expectations/helm/resource.go | 311 + .../expectations/helm/resource_test.go | 309 + .../testutils/expectations/kube/README.md | 351 + .../testutils/expectations/kube/commons.go | 177 + .../testutils/expectations/kube/gomega.go | 34 + .../testutils/expectations/kube/options.go | 61 + .../testutils/expectations/kube/resource.go | 284 + .../expectations/kube/resource_test.go | 419 ++ packages/testutils/go.mod | 150 + packages/testutils/go.sum | 1060 +++ packages/testutils/helpers/README.md | 102 + packages/testutils/helpers/eventually.go | 74 + packages/testutils/helpers/eventually_test.go | 58 + packages/testutils/helpers/kubeutil.go | 143 + packages/testutils/helpers/kubeutil_test.go | 181 + packages/testutils/internal/README.md | 125 + packages/testutils/internal/context/README.md | 13 + .../testutils/internal/context/context.go | 56 + .../internal/context/context_test.go | 37 + packages/testutils/internal/helper.go | 161 + packages/testutils/internal/helper_test.go | 58 + packages/testutils/internal/mockt.go | 25 + packages/testutils/logger/README.md | 43 + packages/testutils/logger/logger.go | 19 + packages/testutils/logger/logger_test.go | 23 + packages/testutils/magefile.go | 9 + packages/testutils/types/README.md | 68 + packages/testutils/types/types.go | 39 + test/integration/go.mod | 118 +- test/integration/go.sum | 845 ++- test/integration/lib/shell/shell.go | 79 + .../lib/testhelpers/component_map.go | 558 ++ .../integration/lib/testhelpers/helmvalues.go | 49 + test/integration/lib/testhelpers/helpers.go | 25 + .../lib/testhelpers/manifestbuilder.go | 341 +- test/integration/lib/testhelpers/types.go | 89 + test/integration/magefile.go | 6 +- .../scenarios/04.workflow/magefile.go | 2 +- .../06.ado/create_update_fallback_test.go | 147 + .../scenarios/06.ado/create_update_test.go | 540 ++ .../scenarios/06.ado/delete_test.go | 175 + .../scenarios/06.ado/manifest/instance.yaml | 9 + .../scenarios/06.ado/manifest/solution.yaml | 6 + .../scenarios/06.ado/manifest/target.yaml | 25 + .../integration/scenarios/06.ado/rbac_test.go | 220 + .../scenarios/06.ado/suite_test.go | 62 + test/localenv/magefile.go | 26 +- 111 files changed, 13597 insertions(+), 3148 deletions(-) create mode 100644 .github/workflows/suite.yml create mode 100644 packages/go.work create mode 100644 packages/go.work.sum delete mode 100644 packages/helm/symphony/azure/metadata.json create mode 100644 packages/helm/symphony/files/metric-middleware.json create mode 100644 packages/helm/symphony/files/trace-middleware.json create mode 100644 packages/helm/symphony/templates/certificate/certificate.yaml create mode 100644 packages/helm/symphony/templates/env-configmap.yaml rename packages/helm/symphony/templates/{ => identity}/extension-identity.yaml (89%) rename packages/helm/symphony/templates/{ => identity}/hooks-cluster-role-binding.yaml (100%) rename packages/helm/symphony/templates/{ => identity}/hooks-cluster-role.yaml (100%) rename packages/helm/symphony/templates/{ => identity}/hooks-service-account.yaml (100%) rename packages/helm/symphony/templates/{ => identity}/pai-cluster-role-binding.yaml (100%) rename packages/helm/symphony/templates/{ => identity}/pai-cluster-role.yaml (100%) rename packages/helm/symphony/templates/{ => identity}/pai-service-account.yaml (100%) create mode 100644 packages/helm/symphony/templates/outbound-proxy-secret.yaml delete mode 100644 packages/helm/symphony/templates/symphony-api.yaml rename packages/helm/symphony/templates/{ => symphony-core}/_helpers.tpl (53%) rename packages/helm/symphony/templates/{ => symphony-core}/delete-job.yaml (100%) rename packages/helm/symphony/templates/{ => symphony-core}/remove-finializers-job.yaml (100%) create mode 100644 packages/helm/symphony/templates/symphony-core/symphony-api.yaml rename packages/helm/symphony/templates/{ => symphony-core}/symphony-service-ext.yaml (61%) create mode 100644 packages/helm/symphony/templates/symphony-core/symphony-service.yaml rename packages/helm/symphony/templates/{symphony.yaml => symphony-core/symphonyk8s.yaml} (98%) delete mode 100644 packages/helm/symphony/templates/symphony-service.yaml create mode 100644 packages/testutils/.gitignore create mode 100644 packages/testutils/.vscode/settings.json create mode 100644 packages/testutils/README.md create mode 100644 packages/testutils/conditions/README.md create mode 100644 packages/testutils/conditions/all.go create mode 100644 packages/testutils/conditions/any.go create mode 100644 packages/testutils/conditions/basic.go create mode 100644 packages/testutils/conditions/condition_test.go create mode 100644 packages/testutils/conditions/count.go create mode 100644 packages/testutils/conditions/jq/README.md create mode 100644 packages/testutils/conditions/jq/common.go create mode 100644 packages/testutils/conditions/jq/jq.go create mode 100644 packages/testutils/conditions/jq/jq_test.go create mode 100644 packages/testutils/conditions/jsonpath/README.md create mode 100644 packages/testutils/conditions/jsonpath/jsonpath.go create mode 100644 packages/testutils/conditions/jsonpath/jsonpath_test.go create mode 100644 packages/testutils/doc.go create mode 100644 packages/testutils/expectations/README.md create mode 100644 packages/testutils/expectations/expectation.go create mode 100644 packages/testutils/expectations/expectation_test.go create mode 100644 packages/testutils/expectations/helm/README.md create mode 100644 packages/testutils/expectations/helm/commons.go create mode 100644 packages/testutils/expectations/helm/gomega.go create mode 100644 packages/testutils/expectations/helm/options.go create mode 100644 packages/testutils/expectations/helm/resource.go create mode 100644 packages/testutils/expectations/helm/resource_test.go create mode 100644 packages/testutils/expectations/kube/README.md create mode 100644 packages/testutils/expectations/kube/commons.go create mode 100644 packages/testutils/expectations/kube/gomega.go create mode 100644 packages/testutils/expectations/kube/options.go create mode 100644 packages/testutils/expectations/kube/resource.go create mode 100644 packages/testutils/expectations/kube/resource_test.go create mode 100644 packages/testutils/go.mod create mode 100644 packages/testutils/go.sum create mode 100644 packages/testutils/helpers/README.md create mode 100644 packages/testutils/helpers/eventually.go create mode 100644 packages/testutils/helpers/eventually_test.go create mode 100644 packages/testutils/helpers/kubeutil.go create mode 100644 packages/testutils/helpers/kubeutil_test.go create mode 100644 packages/testutils/internal/README.md create mode 100644 packages/testutils/internal/context/README.md create mode 100644 packages/testutils/internal/context/context.go create mode 100644 packages/testutils/internal/context/context_test.go create mode 100644 packages/testutils/internal/helper.go create mode 100644 packages/testutils/internal/helper_test.go create mode 100644 packages/testutils/internal/mockt.go create mode 100644 packages/testutils/logger/README.md create mode 100644 packages/testutils/logger/logger.go create mode 100644 packages/testutils/logger/logger_test.go create mode 100644 packages/testutils/magefile.go create mode 100644 packages/testutils/types/README.md create mode 100644 packages/testutils/types/types.go create mode 100644 test/integration/lib/shell/shell.go create mode 100644 test/integration/lib/testhelpers/component_map.go create mode 100644 test/integration/lib/testhelpers/helmvalues.go create mode 100644 test/integration/lib/testhelpers/helpers.go create mode 100644 test/integration/lib/testhelpers/types.go create mode 100644 test/integration/scenarios/06.ado/create_update_fallback_test.go create mode 100644 test/integration/scenarios/06.ado/create_update_test.go create mode 100644 test/integration/scenarios/06.ado/delete_test.go create mode 100644 test/integration/scenarios/06.ado/manifest/instance.yaml create mode 100644 test/integration/scenarios/06.ado/manifest/solution.yaml create mode 100644 test/integration/scenarios/06.ado/manifest/target.yaml create mode 100644 test/integration/scenarios/06.ado/rbac_test.go create mode 100644 test/integration/scenarios/06.ado/suite_test.go diff --git a/.github/workflows/suite.yml b/.github/workflows/suite.yml new file mode 100644 index 000000000..874c3427a --- /dev/null +++ b/.github/workflows/suite.yml @@ -0,0 +1,112 @@ +# 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: suite + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] +env: + ContainerRegistry: "ghcr.io" + ContainerRegistryRepo: "ghcr.io/eclipse-symphony" + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.21 + + - name: Set up custom GOPATH + run: | + mkdir -p /home/runner/go + echo "export GOPATH=/home/runner/go" >> $HOME/.bashrc + echo "export PATH=\$PATH:\$GOPATH/bin" >> $HOME/.bashrc + source $HOME/.bashrc + + - name: Install make + run: sudo apt-get update && sudo apt-get install -y build-essential + + - name: Check docker version and images + run: docker --version && docker images + + - name: Install kubectl + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + sudo mv ./kubectl /usr/local/bin/kubectl + kubectl version --client + kubectl config view + + - name: Install Helm + run: | + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + + - name: Install minikube + run: | + curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 + chmod +x minikube + sudo mv minikube /usr/local/bin/ + minikube start + kubectl config view + + - name: Install Mage + run: | + cd .. + git clone https://github.com/magefile/mage + cd mage + go run bootstrap.go + cd .. + + - name: Install Ginkgo + run: | + go install github.com/onsi/ginkgo/v2/ginkgo@v2.13.1 + export PATH=$PATH:$(go env GOPATH)/bin + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.ContainerRegistry }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build docker images + run: | + cd test/localenv/ + mage build:all + mage cluster:up + + - name: Go work init + run: | + mv go.work.bk go.work + + - name: Run ginkgo suite tests + run: | + cd test/integration/scenarios/06.ado/ + ginkgo --cover --junit-report=junit-suite-tests.xml -r + continue-on-error: true + + - name: Dump SymphonyLogs For ginkgo suite tests + run: | + cd test/localenv/ + mage DumpSymphonyLogsForTest ginkgosuite + continue-on-error: true + + - name: Collect and upload symphony test results + uses: actions/upload-artifact@v2 + with: + name: symphony-suite-result + path: | + test/integration/scenarios/06.ado/junit-suite-tests.xml + /tmp/symhony-integration-test-logs/**/*.log + continue-on-error: true + if: always() \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index a558e26f8..5ec417b81 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -38,4 +38,4 @@ EXPOSE 8080 EXPOSE 8081 ENV LOG_LEVEL Debug # ENV CONFIG /symphony-api.json -CMD exec /symphony-api -c $CONFIG -l $LOG_LEVEL \ No newline at end of file +CMD sh -c 'if [ -f /etc/pki/ca-trust/source/anchors/proxy-cert.crt ]; then update-ca-trust; fi && exec /symphony-api -c $CONFIG -l $LOG_LEVEL' diff --git a/api/symphony-script-over-mqtt.json b/api/symphony-script-over-mqtt.json index 28bbdcd6a..a6f34ab71 100644 --- a/api/symphony-script-over-mqtt.json +++ b/api/symphony-script-over-mqtt.json @@ -26,7 +26,7 @@ "providers.target": "script", "providers.state": "mem-state", "providers.config": "mock-config", - "providers.secret": "mock-secret" + "providers.secret": "mock-secret" }, "providers": { "script": { diff --git a/coa/pkg/apis/v1alpha2/bindings/http/jwt.go b/coa/pkg/apis/v1alpha2/bindings/http/jwt.go index 89097b9ec..ac2ad9e1a 100644 --- a/coa/pkg/apis/v1alpha2/bindings/http/jwt.go +++ b/coa/pkg/apis/v1alpha2/bindings/http/jwt.go @@ -89,19 +89,25 @@ func (j JWT) JWT(next fasthttp.RequestHandler) fasthttp.RequestHandler { } tokenStr := j.readAuthHeader(ctx) if tokenStr == "" { + log.Errorf("JWT: Token is empty.\n") ctx.Response.SetStatusCode(fasthttp.StatusForbidden) } else { if j.AuthServer == AuthServerKuberenetes { + log.Debugf("JWT: Validating token with k8s.\n") err := j.validateServiceAccountToken(ctx, tokenStr) if err != nil { + log.Errorf("JWT: Validate token with k8s failed. %s\n", err.Error()) ctx.Response.SetStatusCode(fasthttp.StatusForbidden) return } next(ctx) } else { + log.Debugf("JWT: Validating token with username plus pwd.\n") _, roles, err := j.validateToken(tokenStr) if err != nil { + log.Error("JWT: Validate token with user creds failed. %s\n", err.Error()) ctx.Response.SetStatusCode(fasthttp.StatusForbidden) + return } else { if j.EnableRBAC { path := string(ctx.Path()) @@ -207,6 +213,7 @@ func (j *JWT) validateToken(tokenStr string) (map[string]interface{}, []string, func (j *JWT) validateServiceAccountToken(ctx *fasthttp.RequestCtx, tokenStr string) error { clientset, err := getKubernetesClient() if err != nil { + log.Errorf("JWT: Could not initialize Kubernetes client.\n") return v1alpha2.NewCOAError(err, "Could not initialize Kubernetes client", v1alpha2.InternalError) } tokenReview := &v1.TokenReview{ @@ -217,11 +224,14 @@ func (j *JWT) validateServiceAccountToken(ctx *fasthttp.RequestCtx, tokenStr str }, }, } + result, err := clientset.AuthenticationV1().TokenReviews().Create(ctx, tokenReview, metav1.CreateOptions{}) if err != nil { + log.Errorf("JWT: Token review using kubernetes api server failed. %s\n", err.Error()) return v1alpha2.NewCOAError(err, "Token review using kubernetes api server failed.", v1alpha2.InternalError) } if !result.Status.Authenticated { + log.Errorf("JWT: Validate token with k8s failed. K8s returned not authenticated.\n") return v1alpha2.NewCOAError(nil, "Authentication failed.", v1alpha2.Unauthorized) } else { apiUsername, err := getApiServiceAccountUsername() @@ -233,6 +243,7 @@ func (j *JWT) validateServiceAccountToken(ctx *fasthttp.RequestCtx, tokenStr str return err } if result.Status.User.Username != apiUsername && result.Status.User.Username != controllerUsername { + log.Errorf("JWT: Validate token with k8s failed. K8s returned invalid username, %s\n", result.Status.User.Username) return v1alpha2.NewCOAError(nil, "Authentication failed.", v1alpha2.Unauthorized) } } diff --git a/docs/symphony-book/build_deployment/build.md b/docs/symphony-book/build_deployment/build.md index 10dfa07a9..6b7f34999 100644 --- a/docs/symphony-book/build_deployment/build.md +++ b/docs/symphony-book/build_deployment/build.md @@ -165,7 +165,7 @@ docker push ghcr.io/eclipse-symphony/symphony-k8s:latest ```bash cd k8s mage helmTemplate -# Generated startup yaml will be updated in ../packages/helm/symphony/templates/symphony.yaml. +# Generated startup yaml will be updated in ../packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml. ``` > **IMPORTANT**: With current Kustomize, empty `creationTimestamp` properties are inserted into the generated artifacts somehow, causing Helm chart to fail. You'll need to manually remove all occurrence of `creationTimestamp` properties with `null` or `"null"` from the artifacts, until a proper solution is found. diff --git a/go.work.bk b/go.work.bk index 531f7f33a..41ff404f5 100644 --- a/go.work.bk +++ b/go.work.bk @@ -1,7 +1,10 @@ go 1.20 use ./api + use ./coa + use ./k8s + use ./cli use ./test/integration \ No newline at end of file diff --git a/go.work.sum b/go.work.sum index f067fe84d..883c7acd4 100644 --- a/go.work.sum +++ b/go.work.sum @@ -368,4 +368,4 @@ k8s.io/gengo v0.0.0-20220902162205-c0856e24416d/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAE k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/kms v0.26.0/go.mod h1:ReC1IEGuxgfN+PDCIpR6w8+XMmDE7uJhxcCwMZFdIYc= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.33/go.mod h1:soWkSNf2tZC7aMibXEqVhCd73GOY5fJikn8qbdzemB0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.33/go.mod h1:soWkSNf2tZC7aMibXEqVhCd73GOY5fJikn8qbdzemB0= \ No newline at end of file diff --git a/k8s/config/oss/default/manager_webhook_patch.yaml b/k8s/config/oss/default/manager_webhook_patch.yaml index 3bacec8b6..074548e92 100644 --- a/k8s/config/oss/default/manager_webhook_patch.yaml +++ b/k8s/config/oss/default/manager_webhook_patch.yaml @@ -18,6 +18,11 @@ spec: name: webhook-server protocol: TCP volumeMounts: + - mountPath: /var/run/secrets/tokens + name: symphony-api-token + - mountPath: '{{ include "symphony.apiServingCertsDir" . }}' + name: api-ca-cert + readOnly: true - mountPath: /tmp/k8s-webhook-server/serving-certs name: cert readOnly: true diff --git a/k8s/config/oss/helm/manager-patch.yaml b/k8s/config/oss/helm/manager-patch.yaml index 521537dee..d44170fea 100644 --- a/k8s/config/oss/helm/manager-patch.yaml +++ b/k8s/config/oss/helm/manager-patch.yaml @@ -28,8 +28,31 @@ spec: value: "{{ .Chart.AppVersion }}" - name: CONFIG_NAME value: '{{ include "symphony.fullname" . }}-manager-config' + - name: SERVICE_ACCOUNT_NAME + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + - name: USE_SERVICE_ACCOUNT_TOKENS + value: "true" + envFrom: + - configMapRef: + name: '{{ include "symphony.envConfigName" . }}' volumes: - name: cert secret: defaultMode: 420 - secretName: '{{ include "symphony.fullname" . }}-webhook-server-cert' \ No newline at end of file + secretName: '{{ include "symphony.fullname" . }}-webhook-server-cert' + - name: symphony-api-token + projected: + sources: + - serviceAccountToken: + audience: '{{ include "symphony.url" . }}' + expirationSeconds: 600 + path: symphony-api-token + - name: api-ca-cert + secret: + defaultMode: 420 + items: + - key: ca.crt + path: ca.crt + secretName: '{{ include "symphony.apiServingCertName" . }}' diff --git a/k8s/magefile.go b/k8s/magefile.go index fd0f1814e..cf538fa90 100644 --- a/k8s/magefile.go +++ b/k8s/magefile.go @@ -99,7 +99,7 @@ func Run() error { // Kustomize startup symphony yaml for helm chart. func HelmTemplate() error { mg.Deps(ensureKustomize, Manifests) - return kustomize.Command("build config/oss/helm -o ../packages/helm/symphony/templates/symphony.yaml").Run() + return kustomize.Command("build config/oss/helm -o ../packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml").Run() } // Install CRDs into the K8s cluster specified in ~/.kube/config. diff --git a/packages/go.work b/packages/go.work new file mode 100644 index 000000000..74a22c9ef --- /dev/null +++ b/packages/go.work @@ -0,0 +1,6 @@ +go 1.19 + +use ( + ./mage + ./testutils +) diff --git a/packages/go.work.sum b/packages/go.work.sum new file mode 100644 index 000000000..717ae5aea --- /dev/null +++ b/packages/go.work.sum @@ -0,0 +1,11 @@ +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/cheggaaa/pb v2.0.7+incompatible h1:gLKifR1UkZ/kLkda5gC0K6c8g+jU2sINPtBeOiNlMhU= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= diff --git a/packages/helm/symphony/Chart.yaml b/packages/helm/symphony/Chart.yaml index 40ba3e818..6bf7c3381 100644 --- a/packages/helm/symphony/Chart.yaml +++ b/packages/helm/symphony/Chart.yaml @@ -8,7 +8,7 @@ version: "0.48.22" appVersion: "0.48.22" dependencies: - name: cert-manager - version: "1.4.0" + version: "1.13.1" repository: "https://charts.jetstack.io" - name: zipkin version: "0.7.0" diff --git a/packages/helm/symphony/azure/metadata.json b/packages/helm/symphony/azure/metadata.json deleted file mode 100644 index 4055ae7f0..000000000 --- a/packages/helm/symphony/azure/metadata.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "mappings": [ - { - "ApiVersion": "2020-01-01-preview", - "ResourceType": "solutions", - "ProviderName": "Private.Symphony", - "ResourceMapping": { - "Version": "v1", - "Group": "symphony.microsoft.com", - "Kind": "Solution", - "Name": "solutions.symphony.microsoft.com" - } - }, - { - "ApiVersion": "2020-01-01-preview", - "ResourceType": "instances", - "ResourceProviderName": "Private.Symphony", - "ResourceMapping": { - "Version": "v1", - "Group": "symphony.microsoft.com", - "Kind": "Instance", - "Name": "instances.symphony.microsoft.com", - "IsAsync": true - } - }, - { - "ApiVersion": "2020-01-01-preview", - "ResourceType": "targets", - "ResourceProviderName": "Private.Symphony", - "ResourceMapping": { - "Version": "v1", - "Group": "symphony.microsoft.com", - "Kind": "Target", - "Name": "targets.symphony.microsoft.com", - "IsAsync": true - } - } - ] -} diff --git a/packages/helm/symphony/crds/cert-manager.crds.yaml b/packages/helm/symphony/crds/cert-manager.crds.yaml index 15af8a5ba..d45337eba 100644 --- a/packages/helm/symphony/crds/cert-manager.crds.yaml +++ b/packages/helm/symphony/crds/cert-manager.crds.yaml @@ -16,31 +16,46 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: clusterissuers.cert-manager.io + name: certificaterequests.cert-manager.io labels: app: 'cert-manager' app.kubernetes.io/name: 'cert-manager' app.kubernetes.io/instance: 'cert-manager' # Generated labels - app.kubernetes.io/version: "v1.11.0" + app.kubernetes.io/version: "v1.13.1" spec: group: cert-manager.io names: - kind: ClusterIssuer - listKind: ClusterIssuerList - plural: clusterissuers - singular: clusterissuer + kind: CertificateRequest + listKind: CertificateRequestList + plural: certificaterequests + shortNames: + - cr + - crs + singular: certificaterequest categories: - cert-manager - scope: Cluster + scope: Namespaced versions: - name: v1 subresources: status: {} additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Approved")].status + name: Approved + type: string + - jsonPath: .status.conditions[?(@.type=="Denied")].status + name: Denied + type: string - jsonPath: .status.conditions[?(@.type=="Ready")].status name: Ready type: string + - jsonPath: .spec.issuerRef.name + name: Issuer + type: string + - jsonPath: .spec.username + name: Requestor + type: string - jsonPath: .status.conditions[?(@.type=="Ready")].message name: Status priority: 1 @@ -51,10 +66,8 @@ spec: type: date schema: openAPIV3Schema: - description: A ClusterIssuer represents a certificate issuing authority which can be referenced as part of `issuerRef` fields. It is similar to an Issuer, however it is cluster-scoped and therefore can be referenced by resources that exist in *any* namespace, not just the same namespace as the referent. + description: "A CertificateRequest is used to request a signed certificate from one of the configured issuers. \n All fields within the CertificateRequest's `spec` are immutable after creation. A CertificateRequest will either succeed or fail, as denoted by its `Ready` status condition and its `status.failureTime` field. \n A CertificateRequest is a one-shot resource, meaning it represents a single point in time request for a certificate and cannot be re-used." type: object - required: - - spec properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' @@ -65,1293 +78,1629 @@ spec: metadata: type: object spec: - description: Desired state of the ClusterIssuer resource. + description: Specification of the desired state of the CertificateRequest resource. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status type: object + required: + - issuerRef + - request properties: - acme: - description: ACME configures this issuer to communicate with a RFC8555 (ACME) server to obtain signed x509 certificates. + duration: + description: Requested 'duration' (i.e. lifetime) of the Certificate. Note that the issuer may choose to ignore the requested duration, just like any other requested attribute. + type: string + extra: + description: Extra contains extra attributes of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable. + type: object + additionalProperties: + type: array + items: + type: string + groups: + description: Groups contains group membership of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable. + type: array + items: + type: string + x-kubernetes-list-type: atomic + isCA: + description: "Requested basic constraints isCA value. Note that the issuer may choose to ignore the requested isCA value, just like any other requested attribute. \n NOTE: If the CSR in the `Request` field has a BasicConstraints extension, it must have the same isCA value as specified here. \n If true, this will automatically add the `cert sign` usage to the list of requested `usages`." + type: boolean + issuerRef: + description: "Reference to the issuer responsible for issuing the certificate. If the issuer is namespace-scoped, it must be in the same namespace as the Certificate. If the issuer is cluster-scoped, it can be used from any namespace. \n The `name` field of the reference must always be specified." type: object required: - - privateKeySecretRef - - server + - name properties: - caBundle: - description: Base64-encoded bundle of PEM CAs which can be used to validate the certificate chain presented by the ACME server. Mutually exclusive with SkipTLSVerify; prefer using CABundle to prevent various kinds of security vulnerabilities. If CABundle and SkipTLSVerify are unset, the system certificate bundle inside the container is used to validate the TLS connection. - type: string - format: byte - disableAccountKeyGeneration: - description: Enables or disables generating a new ACME account key. If true, the Issuer resource will *not* request a new account but will expect the account key to be supplied via an existing secret. If false, the cert-manager system will generate a new ACME account key for the Issuer. Defaults to false. - type: boolean - email: - description: Email is the email address to be associated with the ACME account. This field is optional, but it is strongly recommended to be set. It will be used to contact you in case of issues with your account or certificates, including expiry notification emails. This field may be updated after the account is initially registered. + group: + description: Group of the resource being referred to. type: string - enableDurationFeature: - description: Enables requesting a Not After date on certificates that matches the duration of the certificate. This is not supported by all ACME servers like Let's Encrypt. If set to true when the ACME server does not support it it will create an error on the Order. Defaults to false. - type: boolean - externalAccountBinding: - description: ExternalAccountBinding is a reference to a CA external account of the ACME server. If set, upon registration cert-manager will attempt to associate the given external account credentials with the registered ACME account. - type: object - required: - - keyID - - keySecretRef - properties: - keyAlgorithm: - description: 'Deprecated: keyAlgorithm field exists for historical compatibility reasons and should not be used. The algorithm is now hardcoded to HS256 in golang/x/crypto/acme.' - type: string - enum: - - HS256 - - HS384 - - HS512 - keyID: - description: keyID is the ID of the CA key that the External Account is bound to. - type: string - keySecretRef: - description: keySecretRef is a Secret Key Selector referencing a data item in a Kubernetes Secret which holds the symmetric MAC key of the External Account Binding. The `key` is the index string that is paired with the key data in the Secret and should not be confused with the key data itself, or indeed with the External Account Binding keyID above. The secret key stored in the Secret **must** be un-padded, base64 URL encoded data. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - preferredChain: - description: 'PreferredChain is the chain to use if the ACME server outputs multiple. PreferredChain is no guarantee that this one gets delivered by the ACME endpoint. For example, for Let''s Encrypt''s DST crosssign you would use: "DST Root CA X3" or "ISRG Root X1" for the newer Let''s Encrypt root CA. This value picks the first certificate bundle in the ACME alternative chains that has a certificate with this value as its issuer''s CN' + kind: + description: Kind of the resource being referred to. type: string - maxLength: 64 - privateKeySecretRef: - description: PrivateKey is the name of a Kubernetes Secret resource that will be used to store the automatically generated ACME account private key. Optionally, a `key` may be specified to select a specific entry within the named Secret resource. If `key` is not specified, a default of `tls.key` will be used. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - server: - description: 'Server is the URL used to access the ACME server''s ''directory'' endpoint. For example, for Let''s Encrypt''s staging endpoint, you would use: "https://acme-staging-v02.api.letsencrypt.org/directory". Only ACME v2 endpoints (i.e. RFC 8555) are supported.' + name: + description: Name of the resource being referred to. type: string - skipTLSVerify: - description: 'INSECURE: Enables or disables validation of the ACME server TLS certificate. If true, requests to the ACME server will not have the TLS certificate chain validated. Mutually exclusive with CABundle; prefer using CABundle to prevent various kinds of security vulnerabilities. Only enable this option in development environments. If CABundle and SkipTLSVerify are unset, the system certificate bundle inside the container is used to validate the TLS connection. Defaults to false.' - type: boolean - solvers: - description: 'Solvers is a list of challenge solvers that will be used to solve ACME challenges for the matching domains. Solver configurations must be provided in order to obtain certificates from an ACME server. For more information, see: https://cert-manager.io/docs/configuration/acme/' - type: array - items: - description: An ACMEChallengeSolver describes how to solve ACME challenges for the issuer it is part of. A selector may be provided to use different solving strategies for different DNS names. Only one of HTTP01 or DNS01 must be provided. - type: object - properties: - dns01: - description: Configures cert-manager to attempt to complete authorizations by performing the DNS01 challenge flow. - type: object - properties: - acmeDNS: - description: Use the 'ACME DNS' (https://github.com/joohoi/acme-dns) API to manage DNS01 challenge records. - type: object - required: - - accountSecretRef - - host - properties: - accountSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - host: - type: string - akamai: - description: Use the Akamai DNS zone management API to manage DNS01 challenge records. - type: object - required: - - accessTokenSecretRef - - clientSecretSecretRef - - clientTokenSecretRef - - serviceConsumerDomain - properties: - accessTokenSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - clientSecretSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - clientTokenSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - serviceConsumerDomain: - type: string - azureDNS: - description: Use the Microsoft Azure DNS API to manage DNS01 challenge records. - type: object - required: - - resourceGroupName - - subscriptionID - properties: - clientID: - description: if both this and ClientSecret are left unset MSI will be used - type: string - clientSecretSecretRef: - description: if both this and ClientID are left unset MSI will be used - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - environment: - description: name of the Azure environment (default AzurePublicCloud) - type: string - enum: - - AzurePublicCloud - - AzureChinaCloud - - AzureGermanCloud - - AzureUSGovernmentCloud - hostedZoneName: - description: name of the DNS zone that should be used - type: string - managedIdentity: - description: managed identity configuration, can not be used at the same time as clientID, clientSecretSecretRef or tenantID - type: object - properties: - clientID: - description: client ID of the managed identity, can not be used at the same time as resourceID - type: string - resourceID: - description: resource ID of the managed identity, can not be used at the same time as clientID - type: string - resourceGroupName: - description: resource group the DNS zone is located in - type: string - subscriptionID: - description: ID of the Azure subscription - type: string - tenantID: - description: when specifying ClientID and ClientSecret then this field is also needed - type: string - cloudDNS: - description: Use the Google Cloud DNS API to manage DNS01 challenge records. - type: object - required: - - project - properties: - hostedZoneName: - description: HostedZoneName is an optional field that tells cert-manager in which Cloud DNS zone the challenge record has to be created. If left empty cert-manager will automatically choose a zone. - type: string - project: - type: string - serviceAccountSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - cloudflare: - description: Use the Cloudflare API to manage DNS01 challenge records. - type: object - properties: - apiKeySecretRef: - description: 'API key to use to authenticate with Cloudflare. Note: using an API token to authenticate is now the recommended method as it allows greater control of permissions.' - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - apiTokenSecretRef: - description: API token used to authenticate with Cloudflare. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - email: - description: Email of the account, only required when using API key based authentication. - type: string - cnameStrategy: - description: CNAMEStrategy configures how the DNS01 provider should handle CNAME records when found in DNS zones. + request: + description: "The PEM-encoded X.509 certificate signing request to be submitted to the issuer for signing. \n If the CSR has a BasicConstraints extension, its isCA attribute must match the `isCA` value of this CertificateRequest. If the CSR has a KeyUsage extension, its key usages must match the key usages in the `usages` field of this CertificateRequest. If the CSR has a ExtKeyUsage extension, its extended key usages must match the extended key usages in the `usages` field of this CertificateRequest." + type: string + format: byte + uid: + description: UID contains the uid of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable. + type: string + usages: + description: "Requested key usages and extended key usages. \n NOTE: If the CSR in the `Request` field has uses the KeyUsage or ExtKeyUsage extension, these extensions must have the same values as specified here without any additional values. \n If unset, defaults to `digital signature` and `key encipherment`." + type: array + items: + description: "KeyUsage specifies valid usage contexts for keys. See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3 https://tools.ietf.org/html/rfc5280#section-4.2.1.12 \n Valid KeyUsage values are as follows: \"signing\", \"digital signature\", \"content commitment\", \"key encipherment\", \"key agreement\", \"data encipherment\", \"cert sign\", \"crl sign\", \"encipher only\", \"decipher only\", \"any\", \"server auth\", \"client auth\", \"code signing\", \"email protection\", \"s/mime\", \"ipsec end system\", \"ipsec tunnel\", \"ipsec user\", \"timestamping\", \"ocsp signing\", \"microsoft sgc\", \"netscape sgc\"" + type: string + enum: + - signing + - digital signature + - content commitment + - key encipherment + - key agreement + - data encipherment + - cert sign + - crl sign + - encipher only + - decipher only + - any + - server auth + - client auth + - code signing + - email protection + - s/mime + - ipsec end system + - ipsec tunnel + - ipsec user + - timestamping + - ocsp signing + - microsoft sgc + - netscape sgc + username: + description: Username contains the name of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable. + type: string + status: + description: 'Status of the CertificateRequest. This is set and managed automatically. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + type: object + properties: + ca: + description: The PEM encoded X.509 certificate of the signer, also known as the CA (Certificate Authority). This is set on a best-effort basis by different issuers. If not set, the CA is assumed to be unknown/not available. + type: string + format: byte + certificate: + description: The PEM encoded X.509 certificate resulting from the certificate signing request. If not set, the CertificateRequest has either not been completed or has failed. More information on failure can be found by checking the `conditions` field. + type: string + format: byte + conditions: + description: List of status conditions to indicate the status of a CertificateRequest. Known condition types are `Ready`, `InvalidRequest`, `Approved` and `Denied`. + type: array + items: + description: CertificateRequestCondition contains condition information for a CertificateRequest. + type: object + required: + - status + - type + properties: + lastTransitionTime: + description: LastTransitionTime is the timestamp corresponding to the last status change of this condition. + type: string + format: date-time + message: + description: Message is a human readable description of the details of the last transition, complementing reason. + type: string + reason: + description: Reason is a brief machine readable explanation for the condition's last transition. + type: string + status: + description: Status of the condition, one of (`True`, `False`, `Unknown`). + type: string + enum: + - "True" + - "False" + - Unknown + type: + description: Type of the condition, known values are (`Ready`, `InvalidRequest`, `Approved`, `Denied`). + type: string + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + failureTime: + description: FailureTime stores the time that this CertificateRequest failed. This is used to influence garbage collection and back-off. + type: string + format: date-time + served: true + storage: true +--- +# Source: cert-manager/templates/crds.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: certificates.cert-manager.io + labels: + app: 'cert-manager' + app.kubernetes.io/name: 'cert-manager' + app.kubernetes.io/instance: 'cert-manager' + # Generated labels + app.kubernetes.io/version: "v1.13.1" +spec: + group: cert-manager.io + names: + kind: Certificate + listKind: CertificateList + plural: certificates + shortNames: + - cert + - certs + singular: certificate + categories: + - cert-manager + scope: Namespaced + versions: + - name: v1 + subresources: + status: {} + additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .spec.secretName + name: Secret + type: string + - jsonPath: .spec.issuerRef.name + name: Issuer + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + description: CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + name: Age + type: date + schema: + openAPIV3Schema: + description: "A Certificate resource should be created to ensure an up to date and signed X.509 certificate is stored in the Kubernetes Secret resource named in `spec.secretName`. \n The stored certificate will be renewed before it expires (as configured by `spec.renewBefore`)." + type: object + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Specification of the desired state of the Certificate resource. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + type: object + required: + - issuerRef + - secretName + properties: + additionalOutputFormats: + description: "Defines extra output formats of the private key and signed certificate chain to be written to this Certificate's target Secret. \n This is an Alpha Feature and is only enabled with the `--feature-gates=AdditionalCertificateOutputFormats=true` option set on both the controller and webhook components." + type: array + items: + description: CertificateAdditionalOutputFormat defines an additional output format of a Certificate resource. These contain supplementary data formats of the signed certificate chain and paired private key. + type: object + required: + - type + properties: + type: + description: Type is the name of the format type that should be written to the Certificate's target Secret. + type: string + enum: + - DER + - CombinedPEM + commonName: + description: "Requested common name X509 certificate subject attribute. More info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6 NOTE: TLS clients will ignore this value when any subject alternative name is set (see https://tools.ietf.org/html/rfc6125#section-6.4.4). \n Should have a length of 64 characters or fewer to avoid generating invalid CSRs. Cannot be set if the `literalSubject` field is set." + type: string + dnsNames: + description: Requested DNS subject alternative names. + type: array + items: + type: string + duration: + description: "Requested 'duration' (i.e. lifetime) of the Certificate. Note that the issuer may choose to ignore the requested duration, just like any other requested attribute. \n If unset, this defaults to 90 days. Minimum accepted duration is 1 hour. Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration." + type: string + emailAddresses: + description: Requested email subject alternative names. + type: array + items: + type: string + encodeUsagesInRequest: + description: "Whether the KeyUsage and ExtKeyUsage extensions should be set in the encoded CSR. \n This option defaults to true, and should only be disabled if the target issuer does not support CSRs with these X509 KeyUsage/ ExtKeyUsage extensions." + type: boolean + ipAddresses: + description: Requested IP address subject alternative names. + type: array + items: + type: string + isCA: + description: "Requested basic constraints isCA value. The isCA value is used to set the `isCA` field on the created CertificateRequest resources. Note that the issuer may choose to ignore the requested isCA value, just like any other requested attribute. \n If true, this will automatically add the `cert sign` usage to the list of requested `usages`." + type: boolean + issuerRef: + description: "Reference to the issuer responsible for issuing the certificate. If the issuer is namespace-scoped, it must be in the same namespace as the Certificate. If the issuer is cluster-scoped, it can be used from any namespace. \n The `name` field of the reference must always be specified." + type: object + required: + - name + properties: + group: + description: Group of the resource being referred to. + type: string + kind: + description: Kind of the resource being referred to. + type: string + name: + description: Name of the resource being referred to. + type: string + keystores: + description: Additional keystore output formats to be stored in the Certificate's Secret. + type: object + properties: + jks: + description: JKS configures options for storing a JKS keystore in the `spec.secretName` Secret resource. + type: object + required: + - create + - passwordSecretRef + properties: + create: + description: Create enables JKS keystore creation for the Certificate. If true, a file named `keystore.jks` will be created in the target Secret resource, encrypted using the password stored in `passwordSecretRef`. The keystore file will be updated immediately. If the issuer provided a CA certificate, a file named `truststore.jks` will also be created in the target Secret resource, encrypted using the password stored in `passwordSecretRef` containing the issuing Certificate Authority + type: boolean + passwordSecretRef: + description: PasswordSecretRef is a reference to a key in a Secret resource containing the password used to encrypt the JKS keystore. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + pkcs12: + description: PKCS12 configures options for storing a PKCS12 keystore in the `spec.secretName` Secret resource. + type: object + required: + - create + - passwordSecretRef + properties: + create: + description: Create enables PKCS12 keystore creation for the Certificate. If true, a file named `keystore.p12` will be created in the target Secret resource, encrypted using the password stored in `passwordSecretRef`. The keystore file will be updated immediately. If the issuer provided a CA certificate, a file named `truststore.p12` will also be created in the target Secret resource, encrypted using the password stored in `passwordSecretRef` containing the issuing Certificate Authority + type: boolean + passwordSecretRef: + description: PasswordSecretRef is a reference to a key in a Secret resource containing the password used to encrypt the PKCS12 keystore. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + literalSubject: + description: "Requested X.509 certificate subject, represented using the LDAP \"String Representation of a Distinguished Name\" [1]. Important: the LDAP string format also specifies the order of the attributes in the subject, this is important when issuing certs for LDAP authentication. Example: `CN=foo,DC=corp,DC=example,DC=com` More info [1]: https://datatracker.ietf.org/doc/html/rfc4514 More info: https://github.com/cert-manager/cert-manager/issues/3203 More info: https://github.com/cert-manager/cert-manager/issues/4424 \n Cannot be set if the `subject` or `commonName` field is set. This is an Alpha Feature and is only enabled with the `--feature-gates=LiteralCertificateSubject=true` option set on both the controller and webhook components." + type: string + privateKey: + description: Private key options. These include the key algorithm and size, the used encoding and the rotation policy. + type: object + properties: + algorithm: + description: "Algorithm is the private key algorithm of the corresponding private key for this certificate. \n If provided, allowed values are either `RSA`, `ECDSA` or `Ed25519`. If `algorithm` is specified and `size` is not provided, key size of 2048 will be used for `RSA` key algorithm and key size of 256 will be used for `ECDSA` key algorithm. key size is ignored when using the `Ed25519` key algorithm." + type: string + enum: + - RSA + - ECDSA + - Ed25519 + encoding: + description: "The private key cryptography standards (PKCS) encoding for this certificate's private key to be encoded in. \n If provided, allowed values are `PKCS1` and `PKCS8` standing for PKCS#1 and PKCS#8, respectively. Defaults to `PKCS1` if not specified." + type: string + enum: + - PKCS1 + - PKCS8 + rotationPolicy: + description: "RotationPolicy controls how private keys should be regenerated when a re-issuance is being processed. \n If set to `Never`, a private key will only be generated if one does not already exist in the target `spec.secretName`. If one does exists but it does not have the correct algorithm or size, a warning will be raised to await user intervention. If set to `Always`, a private key matching the specified requirements will be generated whenever a re-issuance occurs. Default is `Never` for backward compatibility." + type: string + enum: + - Never + - Always + size: + description: "Size is the key bit size of the corresponding private key for this certificate. \n If `algorithm` is set to `RSA`, valid values are `2048`, `4096` or `8192`, and will default to `2048` if not specified. If `algorithm` is set to `ECDSA`, valid values are `256`, `384` or `521`, and will default to `256` if not specified. If `algorithm` is set to `Ed25519`, Size is ignored. No other values are allowed." + type: integer + renewBefore: + description: "How long before the currently issued certificate's expiry cert-manager should renew the certificate. For example, if a certificate is valid for 60 minutes, and `renewBefore=10m`, cert-manager will begin to attempt to renew the certificate 50 minutes after it was issued (i.e. when there are 10 minutes remaining until the certificate is no longer valid). \n NOTE: The actual lifetime of the issued certificate is used to determine the renewal time. If an issuer returns a certificate with a different lifetime than the one requested, cert-manager will use the lifetime of the issued certificate. \n If unset, this defaults to 1/3 of the issued certificate's lifetime. Minimum accepted value is 5 minutes. Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration." + type: string + revisionHistoryLimit: + description: "The maximum number of CertificateRequest revisions that are maintained in the Certificate's history. Each revision represents a single `CertificateRequest` created by this Certificate, either when it was created, renewed, or Spec was changed. Revisions will be removed by oldest first if the number of revisions exceeds this number. \n If set, revisionHistoryLimit must be a value of `1` or greater. If unset (`nil`), revisions will not be garbage collected. Default value is `nil`." + type: integer + format: int32 + secretName: + description: Name of the Secret resource that will be automatically created and managed by this Certificate resource. It will be populated with a private key and certificate, signed by the denoted issuer. The Secret resource lives in the same namespace as the Certificate resource. + type: string + secretTemplate: + description: Defines annotations and labels to be copied to the Certificate's Secret. Labels and annotations on the Secret will be changed as they appear on the SecretTemplate when added or removed. SecretTemplate annotations are added in conjunction with, and cannot overwrite, the base set of annotations cert-manager sets on the Certificate's Secret. + type: object + properties: + annotations: + description: Annotations is a key value map to be copied to the target Kubernetes Secret. + type: object + additionalProperties: + type: string + labels: + description: Labels is a key value map to be copied to the target Kubernetes Secret. + type: object + additionalProperties: + type: string + subject: + description: "Requested set of X509 certificate subject attributes. More info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6 \n The common name attribute is specified separately in the `commonName` field. Cannot be set if the `literalSubject` field is set." + type: object + properties: + countries: + description: Countries to be used on the Certificate. + type: array + items: + type: string + localities: + description: Cities to be used on the Certificate. + type: array + items: + type: string + organizationalUnits: + description: Organizational Units to be used on the Certificate. + type: array + items: + type: string + organizations: + description: Organizations to be used on the Certificate. + type: array + items: + type: string + postalCodes: + description: Postal codes to be used on the Certificate. + type: array + items: + type: string + provinces: + description: State/Provinces to be used on the Certificate. + type: array + items: + type: string + serialNumber: + description: Serial number to be used on the Certificate. + type: string + streetAddresses: + description: Street addresses to be used on the Certificate. + type: array + items: + type: string + uris: + description: Requested URI subject alternative names. + type: array + items: + type: string + usages: + description: "Requested key usages and extended key usages. These usages are used to set the `usages` field on the created CertificateRequest resources. If `encodeUsagesInRequest` is unset or set to `true`, the usages will additionally be encoded in the `request` field which contains the CSR blob. \n If unset, defaults to `digital signature` and `key encipherment`." + type: array + items: + description: "KeyUsage specifies valid usage contexts for keys. See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3 https://tools.ietf.org/html/rfc5280#section-4.2.1.12 \n Valid KeyUsage values are as follows: \"signing\", \"digital signature\", \"content commitment\", \"key encipherment\", \"key agreement\", \"data encipherment\", \"cert sign\", \"crl sign\", \"encipher only\", \"decipher only\", \"any\", \"server auth\", \"client auth\", \"code signing\", \"email protection\", \"s/mime\", \"ipsec end system\", \"ipsec tunnel\", \"ipsec user\", \"timestamping\", \"ocsp signing\", \"microsoft sgc\", \"netscape sgc\"" + type: string + enum: + - signing + - digital signature + - content commitment + - key encipherment + - key agreement + - data encipherment + - cert sign + - crl sign + - encipher only + - decipher only + - any + - server auth + - client auth + - code signing + - email protection + - s/mime + - ipsec end system + - ipsec tunnel + - ipsec user + - timestamping + - ocsp signing + - microsoft sgc + - netscape sgc + status: + description: 'Status of the Certificate. This is set and managed automatically. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + type: object + properties: + conditions: + description: List of status conditions to indicate the status of certificates. Known condition types are `Ready` and `Issuing`. + type: array + items: + description: CertificateCondition contains condition information for an Certificate. + type: object + required: + - status + - type + properties: + lastTransitionTime: + description: LastTransitionTime is the timestamp corresponding to the last status change of this condition. + type: string + format: date-time + message: + description: Message is a human readable description of the details of the last transition, complementing reason. + type: string + observedGeneration: + description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Certificate. + type: integer + format: int64 + reason: + description: Reason is a brief machine readable explanation for the condition's last transition. + type: string + status: + description: Status of the condition, one of (`True`, `False`, `Unknown`). + type: string + enum: + - "True" + - "False" + - Unknown + type: + description: Type of the condition, known values are (`Ready`, `Issuing`). + type: string + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + failedIssuanceAttempts: + description: The number of continuous failed issuance attempts up till now. This field gets removed (if set) on a successful issuance and gets set to 1 if unset and an issuance has failed. If an issuance has failed, the delay till the next issuance will be calculated using formula time.Hour * 2 ^ (failedIssuanceAttempts - 1). + type: integer + lastFailureTime: + description: LastFailureTime is set only if the lastest issuance for this Certificate failed and contains the time of the failure. If an issuance has failed, the delay till the next issuance will be calculated using formula time.Hour * 2 ^ (failedIssuanceAttempts - 1). If the latest issuance has succeeded this field will be unset. + type: string + format: date-time + nextPrivateKeySecretName: + description: The name of the Secret resource containing the private key to be used for the next certificate iteration. The keymanager controller will automatically set this field if the `Issuing` condition is set to `True`. It will automatically unset this field when the Issuing condition is not set or False. + type: string + notAfter: + description: The expiration time of the certificate stored in the secret named by this resource in `spec.secretName`. + type: string + format: date-time + notBefore: + description: The time after which the certificate stored in the secret named by this resource in `spec.secretName` is valid. + type: string + format: date-time + renewalTime: + description: RenewalTime is the time at which the certificate will be next renewed. If not set, no upcoming renewal is scheduled. + type: string + format: date-time + revision: + description: "The current 'revision' of the certificate as issued. \n When a CertificateRequest resource is created, it will have the `cert-manager.io/certificate-revision` set to one greater than the current value of this field. \n Upon issuance, this field will be set to the value of the annotation on the CertificateRequest resource used to issue the certificate. \n Persisting the value on the CertificateRequest resource allows the certificates controller to know whether a request is part of an old issuance or if it is part of the ongoing revision's issuance by checking if the revision value in the annotation is greater than this field." + type: integer + served: true + storage: true +--- +# Source: cert-manager/templates/crds.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: challenges.acme.cert-manager.io + labels: + app: 'cert-manager' + app.kubernetes.io/name: 'cert-manager' + app.kubernetes.io/instance: 'cert-manager' + # Generated labels + app.kubernetes.io/version: "v1.13.1" +spec: + group: acme.cert-manager.io + names: + kind: Challenge + listKind: ChallengeList + plural: challenges + singular: challenge + categories: + - cert-manager + - cert-manager-acme + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .spec.dnsName + name: Domain + type: string + - jsonPath: .status.reason + name: Reason + priority: 1 + type: string + - description: CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: Challenge is a type to represent a Challenge request with an ACME server + type: object + required: + - metadata + - spec + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + type: object + required: + - authorizationURL + - dnsName + - issuerRef + - key + - solver + - token + - type + - url + properties: + authorizationURL: + description: The URL to the ACME Authorization resource that this challenge is a part of. + type: string + dnsName: + description: dnsName is the identifier that this challenge is for, e.g. example.com. If the requested DNSName is a 'wildcard', this field MUST be set to the non-wildcard domain, e.g. for `*.example.com`, it must be `example.com`. + type: string + issuerRef: + description: References a properly configured ACME-type Issuer which should be used to create this Challenge. If the Issuer does not exist, processing will be retried. If the Issuer is not an 'ACME' Issuer, an error will be returned and the Challenge will be marked as failed. + type: object + required: + - name + properties: + group: + description: Group of the resource being referred to. + type: string + kind: + description: Kind of the resource being referred to. + type: string + name: + description: Name of the resource being referred to. + type: string + key: + description: 'The ACME challenge key for this challenge For HTTP01 challenges, this is the value that must be responded with to complete the HTTP01 challenge in the format: `.`. For DNS01 challenges, this is the base64 encoded SHA256 sum of the `.` text that must be set as the TXT record content.' + type: string + solver: + description: Contains the domain solving configuration that should be used to solve this challenge resource. + type: object + properties: + dns01: + description: Configures cert-manager to attempt to complete authorizations by performing the DNS01 challenge flow. + type: object + properties: + acmeDNS: + description: Use the 'ACME DNS' (https://github.com/joohoi/acme-dns) API to manage DNS01 challenge records. + type: object + required: + - accountSecretRef + - host + properties: + accountSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + host: + type: string + akamai: + description: Use the Akamai DNS zone management API to manage DNS01 challenge records. + type: object + required: + - accessTokenSecretRef + - clientSecretSecretRef + - clientTokenSecretRef + - serviceConsumerDomain + properties: + accessTokenSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + clientSecretSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + clientTokenSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + serviceConsumerDomain: + type: string + azureDNS: + description: Use the Microsoft Azure DNS API to manage DNS01 challenge records. + type: object + required: + - resourceGroupName + - subscriptionID + properties: + clientID: + description: if both this and ClientSecret are left unset MSI will be used + type: string + clientSecretSecretRef: + description: if both this and ClientID are left unset MSI will be used + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + environment: + description: name of the Azure environment (default AzurePublicCloud) + type: string + enum: + - AzurePublicCloud + - AzureChinaCloud + - AzureGermanCloud + - AzureUSGovernmentCloud + hostedZoneName: + description: name of the DNS zone that should be used + type: string + managedIdentity: + description: managed identity configuration, can not be used at the same time as clientID, clientSecretSecretRef or tenantID + type: object + properties: + clientID: + description: client ID of the managed identity, can not be used at the same time as resourceID + type: string + resourceID: + description: resource ID of the managed identity, can not be used at the same time as clientID + type: string + resourceGroupName: + description: resource group the DNS zone is located in + type: string + subscriptionID: + description: ID of the Azure subscription + type: string + tenantID: + description: when specifying ClientID and ClientSecret then this field is also needed + type: string + cloudDNS: + description: Use the Google Cloud DNS API to manage DNS01 challenge records. + type: object + required: + - project + properties: + hostedZoneName: + description: HostedZoneName is an optional field that tells cert-manager in which Cloud DNS zone the challenge record has to be created. If left empty cert-manager will automatically choose a zone. + type: string + project: + type: string + serviceAccountSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + cloudflare: + description: Use the Cloudflare API to manage DNS01 challenge records. + type: object + properties: + apiKeySecretRef: + description: 'API key to use to authenticate with Cloudflare. Note: using an API token to authenticate is now the recommended method as it allows greater control of permissions.' + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + apiTokenSecretRef: + description: API token used to authenticate with Cloudflare. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + email: + description: Email of the account, only required when using API key based authentication. + type: string + cnameStrategy: + description: CNAMEStrategy configures how the DNS01 provider should handle CNAME records when found in DNS zones. + type: string + enum: + - None + - Follow + digitalocean: + description: Use the DigitalOcean DNS API to manage DNS01 challenge records. + type: object + required: + - tokenSecretRef + properties: + tokenSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + rfc2136: + description: Use RFC2136 ("Dynamic Updates in the Domain Name System") (https://datatracker.ietf.org/doc/rfc2136/) to manage DNS01 challenge records. + type: object + required: + - nameserver + properties: + nameserver: + description: The IP address or hostname of an authoritative DNS server supporting RFC2136 in the form host:port. If the host is an IPv6 address it must be enclosed in square brackets (e.g [2001:db8::1]) ; port is optional. This field is required. + type: string + tsigAlgorithm: + description: 'The TSIG Algorithm configured in the DNS supporting RFC2136. Used only when ``tsigSecretSecretRef`` and ``tsigKeyName`` are defined. Supported values are (case-insensitive): ``HMACMD5`` (default), ``HMACSHA1``, ``HMACSHA256`` or ``HMACSHA512``.' + type: string + tsigKeyName: + description: The TSIG Key name configured in the DNS. If ``tsigSecretSecretRef`` is defined, this field is required. + type: string + tsigSecretSecretRef: + description: The name of the secret containing the TSIG value. If ``tsigKeyName`` is defined, this field is required. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + route53: + description: Use the AWS Route53 API to manage DNS01 challenge records. + type: object + required: + - region + properties: + accessKeyID: + description: 'The AccessKeyID is used for authentication. Cannot be set when SecretAccessKeyID is set. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' + type: string + accessKeyIDSecretRef: + description: 'The SecretAccessKey is used for authentication. If set, pull the AWS access key ID from a key within a Kubernetes Secret. Cannot be set when AccessKeyID is set. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + hostedZoneID: + description: If set, the provider will manage only this zone in Route53 and will not do an lookup using the route53:ListHostedZonesByName api call. + type: string + region: + description: Always set the region when using AccessKeyID and SecretAccessKey + type: string + role: + description: Role is a Role ARN which the Route53 provider will assume using either the explicit credentials AccessKeyID/SecretAccessKey or the inferred credentials from environment variables, shared credentials file or AWS Instance metadata + type: string + secretAccessKeySecretRef: + description: 'The SecretAccessKey is used for authentication. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + webhook: + description: Configure an external webhook based DNS01 challenge solver to manage DNS01 challenge records. + type: object + required: + - groupName + - solverName + properties: + config: + description: Additional configuration that should be passed to the webhook apiserver when challenges are processed. This can contain arbitrary JSON data. Secret values should not be specified in this stanza. If secret values are needed (e.g. credentials for a DNS service), you should use a SecretKeySelector to reference a Secret resource. For details on the schema of this field, consult the webhook provider implementation's documentation. + x-kubernetes-preserve-unknown-fields: true + groupName: + description: The API group name that should be used when POSTing ChallengePayload resources to the webhook apiserver. This should be the same as the GroupName specified in the webhook provider implementation. + type: string + solverName: + description: The name of the solver to use, as defined in the webhook provider implementation. This will typically be the name of the provider, e.g. 'cloudflare'. + type: string + http01: + description: Configures cert-manager to attempt to complete authorizations by performing the HTTP01 challenge flow. It is not possible to obtain certificates for wildcard domain names (e.g. `*.example.com`) using the HTTP01 challenge mechanism. + type: object + properties: + gatewayHTTPRoute: + description: The Gateway API is a sig-network community API that models service networking in Kubernetes (https://gateway-api.sigs.k8s.io/). The Gateway solver will create HTTPRoutes with the specified labels in the same namespace as the challenge. This solver is experimental, and fields / behaviour may change in the future. + type: object + properties: + labels: + description: Custom labels that will be applied to HTTPRoutes created by cert-manager while solving HTTP-01 challenges. + type: object + additionalProperties: type: string - enum: - - None - - Follow - digitalocean: - description: Use the DigitalOcean DNS API to manage DNS01 challenge records. - type: object - required: - - tokenSecretRef - properties: - tokenSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - rfc2136: - description: Use RFC2136 ("Dynamic Updates in the Domain Name System") (https://datatracker.ietf.org/doc/rfc2136/) to manage DNS01 challenge records. - type: object - required: - - nameserver - properties: - nameserver: - description: The IP address or hostname of an authoritative DNS server supporting RFC2136 in the form host:port. If the host is an IPv6 address it must be enclosed in square brackets (e.g [2001:db8::1]) ; port is optional. This field is required. - type: string - tsigAlgorithm: - description: 'The TSIG Algorithm configured in the DNS supporting RFC2136. Used only when ``tsigSecretSecretRef`` and ``tsigKeyName`` are defined. Supported values are (case-insensitive): ``HMACMD5`` (default), ``HMACSHA1``, ``HMACSHA256`` or ``HMACSHA512``.' - type: string - tsigKeyName: - description: The TSIG Key name configured in the DNS. If ``tsigSecretSecretRef`` is defined, this field is required. - type: string - tsigSecretSecretRef: - description: The name of the secret containing the TSIG value. If ``tsigKeyName`` is defined, this field is required. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - route53: - description: Use the AWS Route53 API to manage DNS01 challenge records. - type: object - required: - - region - properties: - accessKeyID: - description: 'The AccessKeyID is used for authentication. Cannot be set when SecretAccessKeyID is set. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' - type: string - accessKeyIDSecretRef: - description: 'The SecretAccessKey is used for authentication. If set, pull the AWS access key ID from a key within a Kubernetes Secret. Cannot be set when AccessKeyID is set. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - hostedZoneID: - description: If set, the provider will manage only this zone in Route53 and will not do an lookup using the route53:ListHostedZonesByName api call. - type: string - region: - description: Always set the region when using AccessKeyID and SecretAccessKey - type: string - role: - description: Role is a Role ARN which the Route53 provider will assume using either the explicit credentials AccessKeyID/SecretAccessKey or the inferred credentials from environment variables, shared credentials file or AWS Instance metadata - type: string - secretAccessKeySecretRef: - description: 'The SecretAccessKey is used for authentication. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - webhook: - description: Configure an external webhook based DNS01 challenge solver to manage DNS01 challenge records. + parentRefs: + description: 'When solving an HTTP-01 challenge, cert-manager creates an HTTPRoute. cert-manager needs to know which parentRefs should be used when creating the HTTPRoute. Usually, the parentRef references a Gateway. See: https://gateway-api.sigs.k8s.io/api-types/httproute/#attaching-to-gateways' + type: array + items: + description: "ParentReference identifies an API object (usually a Gateway) that can be considered a parent of this resource (usually a route). There are two kinds of parent resources with \"Core\" support: \n * Gateway (Gateway conformance profile) * Service (Mesh conformance profile, experimental, ClusterIP Services only) \n This API may be extended in the future to support additional kinds of parent resources. \n The API object must be valid in the cluster; the Group and Kind must be registered in the cluster for this reference to be valid." type: object required: - - groupName - - solverName + - name properties: - config: - description: Additional configuration that should be passed to the webhook apiserver when challenges are processed. This can contain arbitrary JSON data. Secret values should not be specified in this stanza. If secret values are needed (e.g. credentials for a DNS service), you should use a SecretKeySelector to reference a Secret resource. For details on the schema of this field, consult the webhook provider implementation's documentation. - x-kubernetes-preserve-unknown-fields: true - groupName: - description: The API group name that should be used when POSTing ChallengePayload resources to the webhook apiserver. This should be the same as the GroupName specified in the webhook provider implementation. + group: + description: "Group is the group of the referent. When unspecified, \"gateway.networking.k8s.io\" is inferred. To set the core API group (such as for a \"Service\" kind referent), Group must be explicitly set to \"\" (empty string). \n Support: Core" type: string - solverName: - description: The name of the solver to use, as defined in the webhook provider implementation. This will typically be the name of the provider, e.g. 'cloudflare'. + default: gateway.networking.k8s.io + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + kind: + description: "Kind is kind of the referent. \n There are two kinds of parent resources with \"Core\" support: \n * Gateway (Gateway conformance profile) * Service (Mesh conformance profile, experimental, ClusterIP Services only) \n Support for other resources is Implementation-Specific." type: string - http01: - description: Configures cert-manager to attempt to complete authorizations by performing the HTTP01 challenge flow. It is not possible to obtain certificates for wildcard domain names (e.g. `*.example.com`) using the HTTP01 challenge mechanism. - type: object - properties: - gatewayHTTPRoute: - description: The Gateway API is a sig-network community API that models service networking in Kubernetes (https://gateway-api.sigs.k8s.io/). The Gateway solver will create HTTPRoutes with the specified labels in the same namespace as the challenge. This solver is experimental, and fields / behaviour may change in the future. - type: object - properties: - labels: - description: Custom labels that will be applied to HTTPRoutes created by cert-manager while solving HTTP-01 challenges. - type: object - additionalProperties: - type: string - parentRefs: - description: 'When solving an HTTP-01 challenge, cert-manager creates an HTTPRoute. cert-manager needs to know which parentRefs should be used when creating the HTTPRoute. Usually, the parentRef references a Gateway. See: https://gateway-api.sigs.k8s.io/api-types/httproute/#attaching-to-gateways' - type: array - items: - description: "ParentReference identifies an API object (usually a Gateway) that can be considered a parent of this resource (usually a route). The only kind of parent resource with \"Core\" support is Gateway. This API may be extended in the future to support additional kinds of parent resources, such as HTTPRoute. \n The API object must be valid in the cluster; the Group and Kind must be registered in the cluster for this reference to be valid." - type: object - required: - - name - properties: - group: - description: "Group is the group of the referent. When unspecified, \"gateway.networking.k8s.io\" is inferred. To set the core API group (such as for a \"Service\" kind referent), Group must be explicitly set to \"\" (empty string). \n Support: Core" - type: string - default: gateway.networking.k8s.io - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - kind: - description: "Kind is kind of the referent. \n Support: Core (Gateway) \n Support: Implementation-specific (Other Resources)" - type: string - default: Gateway - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - name: - description: "Name is the name of the referent. \n Support: Core" - type: string - maxLength: 253 - minLength: 1 - namespace: - description: "Namespace is the namespace of the referent. When unspecified, this refers to the local namespace of the Route. \n Note that there are specific rules for ParentRefs which cross namespace boundaries. Cross-namespace references are only valid if they are explicitly allowed by something in the namespace they are referring to. For example: Gateway has the AllowedRoutes field, and ReferenceGrant provides a generic way to enable any other kind of cross-namespace reference. \n Support: Core" - type: string - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - port: - description: "Port is the network port this Route targets. It can be interpreted differently based on the type of parent resource. \n When the parent resource is a Gateway, this targets all listeners listening on the specified port that also support this kind of Route(and select this Route). It's not recommended to set `Port` unless the networking behaviors specified in a Route must apply to a specific port as opposed to a listener(s) whose port(s) may be changed. When both Port and SectionName are specified, the name and port of the selected listener must match both specified values. \n Implementations MAY choose to support other parent resources. Implementations supporting other types of parent resources MUST clearly document how/if Port is interpreted. \n For the purpose of status, an attachment is considered successful as long as the parent resource accepts it partially. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Extended \n " - type: integer - format: int32 - maximum: 65535 - minimum: 1 - sectionName: - description: "SectionName is the name of a section within the target resource. In the following resources, SectionName is interpreted as the following: \n * Gateway: Listener Name. When both Port (experimental) and SectionName are specified, the name and port of the selected listener must match both specified values. \n Implementations MAY choose to support attaching Routes to other resources. If that is the case, they MUST clearly document how SectionName is interpreted. \n When unspecified (empty string), this will reference the entire resource. For the purpose of status, an attachment is considered successful if at least one section in the parent resource accepts it. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Core" - type: string - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - serviceType: - description: Optional service type for Kubernetes solver service. Supported values are NodePort or ClusterIP. If unset, defaults to NodePort. + default: Gateway + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + name: + description: "Name is the name of the referent. \n Support: Core" type: string - ingress: - description: The ingress based HTTP01 challenge solver will solve challenges by creating or modifying Ingress resources in order to route requests for '/.well-known/acme-challenge/XYZ' to 'challenge solver' pods that are provisioned by cert-manager for each Challenge to be completed. - type: object - properties: - class: - description: The ingress class to use when creating Ingress resources to solve ACME challenges that use this challenge solver. Only one of 'class' or 'name' may be specified. + maxLength: 253 + minLength: 1 + namespace: + description: "Namespace is the namespace of the referent. When unspecified, this refers to the local namespace of the Route. \n Note that there are specific rules for ParentRefs which cross namespace boundaries. Cross-namespace references are only valid if they are explicitly allowed by something in the namespace they are referring to. For example: Gateway has the AllowedRoutes field, and ReferenceGrant provides a generic way to enable any other kind of cross-namespace reference. \n ParentRefs from a Route to a Service in the same namespace are \"producer\" routes, which apply default routing rules to inbound connections from any namespace to the Service. \n ParentRefs from a Route to a Service in a different namespace are \"consumer\" routes, and these routing rules are only applied to outbound connections originating from the same namespace as the Route, for which the intended destination of the connections are a Service targeted as a ParentRef of the Route. \n Support: Core" type: string - ingressTemplate: - description: Optional ingress template used to configure the ACME challenge solver ingress used for HTTP01 challenges. - type: object - properties: - metadata: - description: ObjectMeta overrides for the ingress used to solve HTTP01 challenges. Only the 'labels' and 'annotations' fields may be set. If labels or annotations overlap with in-built values, the values here will override the in-built values. - type: object - properties: - annotations: - description: Annotations that should be added to the created ACME HTTP01 solver ingress. - type: object - additionalProperties: - type: string - labels: - description: Labels that should be added to the created ACME HTTP01 solver ingress. - type: object - additionalProperties: - type: string - name: - description: The name of the ingress resource that should have ACME challenge solving routes inserted into it in order to solve HTTP01 challenges. This is typically used in conjunction with ingress controllers like ingress-gce, which maintains a 1:1 mapping between external IPs and ingress resources. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + port: + description: "Port is the network port this Route targets. It can be interpreted differently based on the type of parent resource. \n When the parent resource is a Gateway, this targets all listeners listening on the specified port that also support this kind of Route(and select this Route). It's not recommended to set `Port` unless the networking behaviors specified in a Route must apply to a specific port as opposed to a listener(s) whose port(s) may be changed. When both Port and SectionName are specified, the name and port of the selected listener must match both specified values. \n When the parent resource is a Service, this targets a specific port in the Service spec. When both Port (experimental) and SectionName are specified, the name and port of the selected port must match both specified values. \n Implementations MAY choose to support other parent resources. Implementations supporting other types of parent resources MUST clearly document how/if Port is interpreted. \n For the purpose of status, an attachment is considered successful as long as the parent resource accepts it partially. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Extended \n " + type: integer + format: int32 + maximum: 65535 + minimum: 1 + sectionName: + description: "SectionName is the name of a section within the target resource. In the following resources, SectionName is interpreted as the following: \n * Gateway: Listener Name. When both Port (experimental) and SectionName are specified, the name and port of the selected listener must match both specified values. * Service: Port Name. When both Port (experimental) and SectionName are specified, the name and port of the selected listener must match both specified values. Note that attaching Routes to Services as Parents is part of experimental Mesh support and is not supported for any other purpose. \n Implementations MAY choose to support attaching Routes to other resources. If that is the case, they MUST clearly document how SectionName is interpreted. \n When unspecified (empty string), this will reference the entire resource. For the purpose of status, an attachment is considered successful if at least one section in the parent resource accepts it. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Core" type: string - podTemplate: - description: Optional pod template used to configure the ACME challenge solver pods used for HTTP01 challenges. - type: object - properties: - metadata: - description: ObjectMeta overrides for the pod used to solve HTTP01 challenges. Only the 'labels' and 'annotations' fields may be set. If labels or annotations overlap with in-built values, the values here will override the in-built values. - type: object - properties: - annotations: - description: Annotations that should be added to the create ACME HTTP01 solver pods. - type: object - additionalProperties: - type: string - labels: - description: Labels that should be added to the created ACME HTTP01 solver pods. - type: object - additionalProperties: - type: string - spec: - description: PodSpec defines overrides for the HTTP01 challenge solver pod. Only the 'priorityClassName', 'nodeSelector', 'affinity', 'serviceAccountName' and 'tolerations' fields are supported currently. All other fields will be ignored. - type: object - properties: - affinity: - description: If specified, the pod's scheduling constraints - type: object - properties: - nodeAffinity: - description: Describes node affinity scheduling rules for the pod. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + serviceType: + description: Optional service type for Kubernetes solver service. Supported values are NodePort or ClusterIP. If unset, defaults to NodePort. + type: string + ingress: + description: The ingress based HTTP01 challenge solver will solve challenges by creating or modifying Ingress resources in order to route requests for '/.well-known/acme-challenge/XYZ' to 'challenge solver' pods that are provisioned by cert-manager for each Challenge to be completed. + type: object + properties: + class: + description: This field configures the annotation `kubernetes.io/ingress.class` when creating Ingress resources to solve ACME challenges that use this challenge solver. Only one of `class`, `name` or `ingressClassName` may be specified. + type: string + ingressClassName: + description: This field configures the field `ingressClassName` on the created Ingress resources used to solve ACME challenges that use this challenge solver. This is the recommended way of configuring the ingress class. Only one of `class`, `name` or `ingressClassName` may be specified. + type: string + ingressTemplate: + description: Optional ingress template used to configure the ACME challenge solver ingress used for HTTP01 challenges. + type: object + properties: + metadata: + description: ObjectMeta overrides for the ingress used to solve HTTP01 challenges. Only the 'labels' and 'annotations' fields may be set. If labels or annotations overlap with in-built values, the values here will override the in-built values. + type: object + properties: + annotations: + description: Annotations that should be added to the created ACME HTTP01 solver ingress. + type: object + additionalProperties: + type: string + labels: + description: Labels that should be added to the created ACME HTTP01 solver ingress. + type: object + additionalProperties: + type: string + name: + description: The name of the ingress resource that should have ACME challenge solving routes inserted into it in order to solve HTTP01 challenges. This is typically used in conjunction with ingress controllers like ingress-gce, which maintains a 1:1 mapping between external IPs and ingress resources. Only one of `class`, `name` or `ingressClassName` may be specified. + type: string + podTemplate: + description: Optional pod template used to configure the ACME challenge solver pods used for HTTP01 challenges. + type: object + properties: + metadata: + description: ObjectMeta overrides for the pod used to solve HTTP01 challenges. Only the 'labels' and 'annotations' fields may be set. If labels or annotations overlap with in-built values, the values here will override the in-built values. + type: object + properties: + annotations: + description: Annotations that should be added to the create ACME HTTP01 solver pods. + type: object + additionalProperties: + type: string + labels: + description: Labels that should be added to the created ACME HTTP01 solver pods. + type: object + additionalProperties: + type: string + spec: + description: PodSpec defines overrides for the HTTP01 challenge solver pod. Check ACMEChallengeSolverHTTP01IngressPodSpec to find out currently supported fields. All other fields will be ignored. + type: object + properties: + affinity: + description: If specified, the pod's scheduling constraints + type: object + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the pod. + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node matches the corresponding matchExpressions; the node(s) with the highest sum are the most preferred. + type: array + items: + description: An empty preferred scheduling term matches all objects with implicit weight 0 (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). type: object + required: + - preference + - weight properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node matches the corresponding matchExpressions; the node(s) with the highest sum are the most preferred. - type: array - items: - description: An empty preferred scheduling term matches all objects with implicit weight 0 (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). - type: object - required: - - preference - - weight - properties: - preference: - description: A node selector term, associated with the corresponding weight. - type: object - properties: - matchExpressions: - description: A list of node selector requirements by node's labels. - type: array - items: - description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: The label key that the selector applies to. - type: string - operator: - description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchFields: - description: A list of node selector requirements by node's fields. - type: array - items: - description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: The label key that the selector applies to. - type: string - operator: - description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. - type: array - items: - type: string - x-kubernetes-map-type: atomic - weight: - description: Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100. - type: integer - format: int32 - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to an update), the system may or may not try to eventually evict the pod from its node. + preference: + description: A node selector term, associated with the corresponding weight. type: object - required: - - nodeSelectorTerms properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. The terms are ORed. + matchExpressions: + description: A list of node selector requirements by node's labels. type: array items: - description: A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. - type: object - properties: - matchExpressions: - description: A list of node selector requirements by node's labels. - type: array - items: - description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: The label key that the selector applies to. - type: string - operator: - description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchFields: - description: A list of node selector requirements by node's fields. - type: array - items: - description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: The label key that the selector applies to. - type: string - operator: - description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. - type: array - items: - type: string - x-kubernetes-map-type: atomic - x-kubernetes-map-type: atomic - podAffinity: - description: Describes pod affinity scheduling rules (e.g. co-locate this pod in the same node, zone, etc. as some other pod(s)). - type: object - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. - type: array - items: - description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s) - type: object - required: - - podAffinityTerm - - weight - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated with the corresponding weight. + description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. type: object required: - - topologyKey - properties: - labelSelector: - description: A label query over a set of resources, in this case pods. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: - type: string - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: - type: string - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". + - key + - operator + properties: + key: + description: The label key that the selector applies to. + type: string + operator: + description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. type: array items: type: string - topologyKey: - description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. - type: string - weight: - description: weight associated with matching the corresponding podAffinityTerm, in the range 1-100. - type: integer - format: int32 - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied. - type: array - items: - description: Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running - type: object - required: - - topologyKey - properties: - labelSelector: - description: A label query over a set of resources, in this case pods. + matchFields: + description: A list of node selector requirements by node's fields. + type: array + items: + description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. type: object + required: + - key + - operator properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + key: + description: The label key that the selector applies to. + type: string + operator: + description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. type: array items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: type: string - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100. + type: integer + format: int32 + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to an update), the system may or may not try to eventually evict the pod from its node. + type: object + required: + - nodeSelectorTerms + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The terms are ORed. + type: array + items: + description: A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + type: object + properties: + matchExpressions: + description: A list of node selector requirements by node's labels. + type: array + items: + description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. type: object + required: + - key + - operator properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + key: + description: The label key that the selector applies to. + type: string + operator: + description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. type: array items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: type: string - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". - type: array - items: - type: string - topologyKey: - description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. - type: string - podAntiAffinity: - description: Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s)). - type: object - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to nodes that satisfy the anti-affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling anti-affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. - type: array - items: - description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s) - type: object - required: - - podAffinityTerm - - weight - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated with the corresponding weight. + matchFields: + description: A list of node selector requirements by node's fields. + type: array + items: + description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. type: object required: - - topologyKey + - key + - operator properties: - labelSelector: - description: A label query over a set of resources, in this case pods. + key: + description: The label key that the selector applies to. + type: string + operator: + description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. + type: array + items: + type: string + x-kubernetes-map-type: atomic + x-kubernetes-map-type: atomic + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate this pod in the same node, zone, etc. as some other pod(s)). + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. + type: array + items: + description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s) + type: object + required: + - podAffinityTerm + - weight + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated with the corresponding weight. + type: object + required: + - topologyKey + properties: + labelSelector: + description: A label query over a set of resources, in this case pods. + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. type: object + required: + - key + - operator properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. type: array items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: type: string - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: + type: string + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. type: object + required: + - key + - operator properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. type: array items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: type: string - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". - type: array - items: - type: string - topologyKey: - description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: type: string - weight: - description: weight associated with matching the corresponding podAffinityTerm, in the range 1-100. - type: integer - format: int32 - requiredDuringSchedulingIgnoredDuringExecution: - description: If the anti-affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the anti-affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied. - type: array - items: - description: Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running - type: object - required: - - topologyKey - properties: - labelSelector: - description: A label query over a set of resources, in this case pods. + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". + type: array + items: + type: string + topologyKey: + description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. + type: string + weight: + description: weight associated with matching the corresponding podAffinityTerm, in the range 1-100. + type: integer + format: int32 + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied. + type: array + items: + description: Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running + type: object + required: + - topologyKey + properties: + labelSelector: + description: A label query over a set of resources, in this case pods. + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. type: object + required: + - key + - operator properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. type: array items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: type: string - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: + type: string + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. type: object + required: + - key + - operator properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. type: array items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: key is the label key that the selector applies to. + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: + type: string + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". + type: array + items: + type: string + topologyKey: + description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. + type: string + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s)). + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes that satisfy the anti-affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling anti-affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. + type: array + items: + description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s) + type: object + required: + - podAffinityTerm + - weight + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated with the corresponding weight. + type: object + required: + - topologyKey + properties: + labelSelector: + description: A label query over a set of resources, in this case pods. + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: + type: string + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: - type: string - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". - type: array - items: - type: string - topologyKey: - description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: + type: string + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". + type: array + items: type: string - nodeSelector: - description: 'NodeSelector is a selector which must be true for the pod to fit on a node. Selector which must match a node''s labels for the pod to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/' - type: object - additionalProperties: - type: string - priorityClassName: - description: If specified, the pod's priorityClassName. - type: string - serviceAccountName: - description: If specified, the pod's service account - type: string - tolerations: - description: If specified, the pod's tolerations. - type: array - items: - description: The pod this Toleration is attached to tolerates any taint that matches the triple using the matching operator . - type: object - properties: - effect: - description: Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys. - type: string - operator: - description: Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category. - type: string - tolerationSeconds: - description: TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system. - type: integer - format: int64 - value: - description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string. - type: string - serviceType: - description: Optional service type for Kubernetes solver service. Supported values are NodePort or ClusterIP. If unset, defaults to NodePort. - type: string - selector: - description: Selector selects a set of DNSNames on the Certificate resource that should be solved using this challenge solver. If not specified, the solver will be treated as the 'default' solver with the lowest priority, i.e. if any other solver has a more specific match, it will be used instead. - type: object - properties: - dnsNames: - description: List of DNSNames that this solver will be used to solve. If specified and a match is found, a dnsNames selector will take precedence over a dnsZones selector. If multiple solvers match with the same dnsNames value, the solver with the most matching labels in matchLabels will be selected. If neither has more matches, the solver defined earlier in the list will be selected. - type: array - items: - type: string - dnsZones: - description: List of DNSZones that this solver will be used to solve. The most specific DNS zone match specified here will take precedence over other DNS zone matches, so a solver specifying sys.example.com will be selected over one specifying example.com for the domain www.sys.example.com. If multiple solvers match with the same dnsZones value, the solver with the most matching labels in matchLabels will be selected. If neither has more matches, the solver defined earlier in the list will be selected. - type: array - items: - type: string - matchLabels: - description: A label selector that is used to refine the set of certificate's that this challenge solver will apply to. - type: object - additionalProperties: - type: string - ca: - description: CA configures this issuer to sign certificates using a signing CA keypair stored in a Secret resource. This is used to build internal PKIs that are managed by cert-manager. - type: object - required: - - secretName - properties: - crlDistributionPoints: - description: The CRL distribution points is an X.509 v3 certificate extension which identifies the location of the CRL from which the revocation of this certificate can be checked. If not set, certificates will be issued without distribution points set. - type: array - items: - type: string - ocspServers: - description: The OCSP server list is an X.509 v3 extension that defines a list of URLs of OCSP responders. The OCSP responders can be queried for the revocation status of an issued certificate. If not set, the certificate will be issued with no OCSP servers set. For example, an OCSP server URL could be "http://ocsp.int-x3.letsencrypt.org". - type: array - items: - type: string - secretName: - description: SecretName is the name of the secret used to sign Certificates issued by this Issuer. - type: string - selfSigned: - description: SelfSigned configures this issuer to 'self sign' certificates using the private key used to create the CertificateRequest object. - type: object - properties: - crlDistributionPoints: - description: The CRL distribution points is an X.509 v3 certificate extension which identifies the location of the CRL from which the revocation of this certificate can be checked. If not set certificate will be issued without CDP. Values are strings. - type: array - items: - type: string - vault: - description: Vault configures this issuer to sign certificates using a HashiCorp Vault PKI backend. - type: object - required: - - auth - - path - - server - properties: - auth: - description: Auth configures how cert-manager authenticates with the Vault server. - type: object - properties: - appRole: - description: AppRole authenticates with Vault using the App Role auth mechanism, with the role and secret stored in a Kubernetes Secret resource. - type: object - required: - - path - - roleId - - secretRef - properties: - path: - description: 'Path where the App Role authentication backend is mounted in Vault, e.g: "approle"' - type: string - roleId: - description: RoleID configured in the App Role authentication backend when setting up the authentication backend in Vault. - type: string - secretRef: - description: Reference to a key in a Secret that contains the App Role secret used to authenticate with Vault. The `key` field must be specified and denotes which entry within the Secret resource is used as the app role secret. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - kubernetes: - description: Kubernetes authenticates with Vault by passing the ServiceAccount token stored in the named Secret resource to the Vault server. - type: object - required: - - role - - secretRef - properties: - mountPath: - description: The Vault mountPath here is the mount path to use when authenticating with Vault. For example, setting a value to `/v1/auth/foo`, will use the path `/v1/auth/foo/login` to authenticate with Vault. If unspecified, the default value "/v1/auth/kubernetes" will be used. - type: string - role: - description: A required field containing the Vault Role to assume. A Role binds a Kubernetes ServiceAccount with a set of Vault policies. - type: string - secretRef: - description: The required Secret field containing a Kubernetes ServiceAccount JWT used for authenticating with Vault. Use of 'ambient credentials' is not supported. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - tokenSecretRef: - description: TokenSecretRef authenticates with Vault by presenting a token. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - caBundle: - description: Base64-encoded bundle of PEM CAs which will be used to validate the certificate chain presented by Vault. Only used if using HTTPS to connect to Vault and ignored for HTTP connections. Mutually exclusive with CABundleSecretRef. If neither CABundle nor CABundleSecretRef are defined, the certificate bundle in the cert-manager controller container is used to validate the TLS connection. - type: string - format: byte - caBundleSecretRef: - description: Reference to a Secret containing a bundle of PEM-encoded CAs to use when verifying the certificate chain presented by Vault when using HTTPS. Mutually exclusive with CABundle. If neither CABundle nor CABundleSecretRef are defined, the certificate bundle in the cert-manager controller container is used to validate the TLS connection. If no key for the Secret is specified, cert-manager will default to 'ca.crt'. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - namespace: - description: 'Name of the vault namespace. Namespaces is a set of features within Vault Enterprise that allows Vault environments to support Secure Multi-tenancy. e.g: "ns1" More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces' - type: string - path: - description: 'Path is the mount path of the Vault PKI backend''s `sign` endpoint, e.g: "my_pki_mount/sign/my-role-name".' - type: string - server: - description: 'Server is the connection address for the Vault server, e.g: "https://vault.example.com:8200".' - type: string - venafi: - description: Venafi configures this issuer to sign certificates using a Venafi TPP or Venafi Cloud policy zone. - type: object - required: - - zone - properties: - cloud: - description: Cloud specifies the Venafi cloud configuration settings. Only one of TPP or Cloud may be specified. - type: object - required: - - apiTokenSecretRef - properties: - apiTokenSecretRef: - description: APITokenSecretRef is a secret key selector for the Venafi Cloud API token. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + topologyKey: + description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. + type: string + weight: + description: weight associated with matching the corresponding podAffinityTerm, in the range 1-100. + type: integer + format: int32 + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the anti-affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied. + type: array + items: + description: Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running + type: object + required: + - topologyKey + properties: + labelSelector: + description: A label query over a set of resources, in this case pods. + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: + type: string + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: + type: string + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". + type: array + items: + type: string + topologyKey: + description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. + type: string + imagePullSecrets: + description: If specified, the pod's imagePullSecrets + type: array + items: + description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. + type: object + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + x-kubernetes-map-type: atomic + nodeSelector: + description: 'NodeSelector is a selector which must be true for the pod to fit on a node. Selector which must match a node''s labels for the pod to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/' + type: object + additionalProperties: + type: string + priorityClassName: + description: If specified, the pod's priorityClassName. + type: string + serviceAccountName: + description: If specified, the pod's service account + type: string + tolerations: + description: If specified, the pod's tolerations. + type: array + items: + description: The pod this Toleration is attached to tolerates any taint that matches the triple using the matching operator . + type: object + properties: + effect: + description: Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system. + type: integer + format: int64 + value: + description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + serviceType: + description: Optional service type for Kubernetes solver service. Supported values are NodePort or ClusterIP. If unset, defaults to NodePort. type: string - url: - description: URL is the base URL for Venafi Cloud. Defaults to "https://api.venafi.cloud/v1". - type: string - tpp: - description: TPP specifies Trust Protection Platform configuration settings. Only one of TPP or Cloud may be specified. + selector: + description: Selector selects a set of DNSNames on the Certificate resource that should be solved using this challenge solver. If not specified, the solver will be treated as the 'default' solver with the lowest priority, i.e. if any other solver has a more specific match, it will be used instead. type: object - required: - - credentialsRef - - url properties: - caBundle: - description: Base64-encoded bundle of PEM CAs which will be used to validate the certificate chain presented by the TPP server. Only used if using HTTPS; ignored for HTTP. If undefined, the certificate bundle in the cert-manager controller container is used to validate the chain. - type: string - format: byte - credentialsRef: - description: CredentialsRef is a reference to a Secret containing the username and password for the TPP server. The secret must contain two keys, 'username' and 'password'. + dnsNames: + description: List of DNSNames that this solver will be used to solve. If specified and a match is found, a dnsNames selector will take precedence over a dnsZones selector. If multiple solvers match with the same dnsNames value, the solver with the most matching labels in matchLabels will be selected. If neither has more matches, the solver defined earlier in the list will be selected. + type: array + items: + type: string + dnsZones: + description: List of DNSZones that this solver will be used to solve. The most specific DNS zone match specified here will take precedence over other DNS zone matches, so a solver specifying sys.example.com will be selected over one specifying example.com for the domain www.sys.example.com. If multiple solvers match with the same dnsZones value, the solver with the most matching labels in matchLabels will be selected. If neither has more matches, the solver defined earlier in the list will be selected. + type: array + items: + type: string + matchLabels: + description: A label selector that is used to refine the set of certificate's that this challenge solver will apply to. type: object - required: - - name - properties: - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - url: - description: 'URL is the base URL for the vedsdk endpoint of the Venafi TPP instance, for example: "https://tpp.example.com/vedsdk".' - type: string - zone: - description: Zone is the Venafi Policy Zone to use for this issuer. All requests made to the Venafi platform will be restricted by the named zone policy. This field is required. - type: string + additionalProperties: + type: string + token: + description: The ACME challenge token for this challenge. This is the raw value returned from the ACME server. + type: string + type: + description: The type of ACME challenge this resource represents. One of "HTTP-01" or "DNS-01". + type: string + enum: + - HTTP-01 + - DNS-01 + url: + description: The URL of the ACME Challenge resource for this challenge. This can be used to lookup details about the status of this challenge. + type: string + wildcard: + description: wildcard will be true if this challenge is for a wildcard identifier, for example '*.example.com'. + type: boolean status: - description: Status of the ClusterIssuer. This is set and managed automatically. type: object properties: - acme: - description: ACME specific status options. This field should only be set if the Issuer is configured to use an ACME server to issue certificates. - type: object - properties: - lastRegisteredEmail: - description: LastRegisteredEmail is the email associated with the latest registered ACME account, in order to track changes made to registered account associated with the Issuer - type: string - uri: - description: URI is the unique account identifier, which can also be used to retrieve account details from the CA - type: string - conditions: - description: List of status conditions to indicate the status of a CertificateRequest. Known condition types are `Ready`. - type: array - items: - description: IssuerCondition contains condition information for an Issuer. - type: object - required: - - status - - type - properties: - lastTransitionTime: - description: LastTransitionTime is the timestamp corresponding to the last status change of this condition. - type: string - format: date-time - message: - description: Message is a human readable description of the details of the last transition, complementing reason. - type: string - observedGeneration: - description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Issuer. - type: integer - format: int64 - reason: - description: Reason is a brief machine readable explanation for the condition's last transition. - type: string - status: - description: Status of the condition, one of (`True`, `False`, `Unknown`). - type: string - enum: - - "True" - - "False" - - Unknown - type: - description: Type of the condition, known values are (`Ready`). - type: string - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map + presented: + description: presented will be set to true if the challenge values for this challenge are currently 'presented'. This *does not* imply the self check is passing. Only that the values have been 'submitted' for the appropriate challenge mechanism (i.e. the DNS01 TXT record has been presented, or the HTTP01 configuration has been configured). + type: boolean + processing: + description: Used to denote whether this challenge should be processed or not. This field will only be set to true by the 'scheduling' component. It will only be set to false by the 'challenges' controller, after the challenge has reached a final state or timed out. If this field is set to false, the challenge controller will not take any more action. + type: boolean + reason: + description: Contains human readable information on why the Challenge is in the current state. + type: string + state: + description: Contains the current 'state' of the challenge. If not set, the state of the challenge is unknown. + type: string + enum: + - valid + - ready + - pending + - processing + - invalid + - expired + - errored served: true storage: true + subresources: + status: {} --- # Source: cert-manager/templates/crds.yaml apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: challenges.acme.cert-manager.io + name: clusterissuers.cert-manager.io labels: app: 'cert-manager' app.kubernetes.io/name: 'cert-manager' - app.kubernetes.io/instance: 'cert-manager' + app.kubernetes.io/instance: "cert-manager" # Generated labels - app.kubernetes.io/version: "v1.11.0" + app.kubernetes.io/version: "v1.13.1" spec: - group: acme.cert-manager.io + group: cert-manager.io names: - kind: Challenge - listKind: ChallengeList - plural: challenges - singular: challenge - categories: - - cert-manager - - cert-manager-acme - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .status.state - name: State - type: string - - jsonPath: .spec.dnsName - name: Domain + kind: ClusterIssuer + listKind: ClusterIssuerList + plural: clusterissuers + singular: clusterissuer + categories: + - cert-manager + scope: Cluster + versions: + - name: v1 + subresources: + status: {} + additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready type: string - - jsonPath: .status.reason - name: Reason + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status priority: 1 type: string - - description: CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. - jsonPath: .metadata.creationTimestamp + - jsonPath: .metadata.creationTimestamp + description: CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. name: Age type: date - name: v1 schema: openAPIV3Schema: - description: Challenge is a type to represent a Challenge request with an ACME server + description: A ClusterIssuer represents a certificate issuing authority which can be referenced as part of `issuerRef` fields. It is similar to an Issuer, however it is cluster-scoped and therefore can be referenced by resources that exist in *any* namespace, not just the same namespace as the referent. type: object required: - - metadata - spec properties: apiVersion: @@ -1363,1179 +1712,1238 @@ spec: metadata: type: object spec: + description: Desired state of the ClusterIssuer resource. type: object - required: - - authorizationURL - - dnsName - - issuerRef - - key - - solver - - token - - type - - url properties: - authorizationURL: - description: The URL to the ACME Authorization resource that this challenge is a part of. - type: string - dnsName: - description: dnsName is the identifier that this challenge is for, e.g. example.com. If the requested DNSName is a 'wildcard', this field MUST be set to the non-wildcard domain, e.g. for `*.example.com`, it must be `example.com`. - type: string - issuerRef: - description: References a properly configured ACME-type Issuer which should be used to create this Challenge. If the Issuer does not exist, processing will be retried. If the Issuer is not an 'ACME' Issuer, an error will be returned and the Challenge will be marked as failed. + acme: + description: ACME configures this issuer to communicate with a RFC8555 (ACME) server to obtain signed x509 certificates. type: object required: - - name + - privateKeySecretRef + - server properties: - group: - description: Group of the resource being referred to. - type: string - kind: - description: Kind of the resource being referred to. + caBundle: + description: Base64-encoded bundle of PEM CAs which can be used to validate the certificate chain presented by the ACME server. Mutually exclusive with SkipTLSVerify; prefer using CABundle to prevent various kinds of security vulnerabilities. If CABundle and SkipTLSVerify are unset, the system certificate bundle inside the container is used to validate the TLS connection. type: string - name: - description: Name of the resource being referred to. + format: byte + disableAccountKeyGeneration: + description: Enables or disables generating a new ACME account key. If true, the Issuer resource will *not* request a new account but will expect the account key to be supplied via an existing secret. If false, the cert-manager system will generate a new ACME account key for the Issuer. Defaults to false. + type: boolean + email: + description: Email is the email address to be associated with the ACME account. This field is optional, but it is strongly recommended to be set. It will be used to contact you in case of issues with your account or certificates, including expiry notification emails. This field may be updated after the account is initially registered. type: string - key: - description: 'The ACME challenge key for this challenge For HTTP01 challenges, this is the value that must be responded with to complete the HTTP01 challenge in the format: `.`. For DNS01 challenges, this is the base64 encoded SHA256 sum of the `.` text that must be set as the TXT record content.' - type: string - solver: - description: Contains the domain solving configuration that should be used to solve this challenge resource. - type: object - properties: - dns01: - description: Configures cert-manager to attempt to complete authorizations by performing the DNS01 challenge flow. + enableDurationFeature: + description: Enables requesting a Not After date on certificates that matches the duration of the certificate. This is not supported by all ACME servers like Let's Encrypt. If set to true when the ACME server does not support it it will create an error on the Order. Defaults to false. + type: boolean + externalAccountBinding: + description: ExternalAccountBinding is a reference to a CA external account of the ACME server. If set, upon registration cert-manager will attempt to associate the given external account credentials with the registered ACME account. type: object + required: + - keyID + - keySecretRef properties: - acmeDNS: - description: Use the 'ACME DNS' (https://github.com/joohoi/acme-dns) API to manage DNS01 challenge records. - type: object - required: - - accountSecretRef - - host - properties: - accountSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - host: - type: string - akamai: - description: Use the Akamai DNS zone management API to manage DNS01 challenge records. - type: object - required: - - accessTokenSecretRef - - clientSecretSecretRef - - clientTokenSecretRef - - serviceConsumerDomain - properties: - accessTokenSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - clientSecretSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - clientTokenSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - serviceConsumerDomain: - type: string - azureDNS: - description: Use the Microsoft Azure DNS API to manage DNS01 challenge records. - type: object - required: - - resourceGroupName - - subscriptionID - properties: - clientID: - description: if both this and ClientSecret are left unset MSI will be used - type: string - clientSecretSecretRef: - description: if both this and ClientID are left unset MSI will be used - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - environment: - description: name of the Azure environment (default AzurePublicCloud) - type: string - enum: - - AzurePublicCloud - - AzureChinaCloud - - AzureGermanCloud - - AzureUSGovernmentCloud - hostedZoneName: - description: name of the DNS zone that should be used - type: string - managedIdentity: - description: managed identity configuration, can not be used at the same time as clientID, clientSecretSecretRef or tenantID - type: object - properties: - clientID: - description: client ID of the managed identity, can not be used at the same time as resourceID - type: string - resourceID: - description: resource ID of the managed identity, can not be used at the same time as clientID - type: string - resourceGroupName: - description: resource group the DNS zone is located in - type: string - subscriptionID: - description: ID of the Azure subscription - type: string - tenantID: - description: when specifying ClientID and ClientSecret then this field is also needed - type: string - cloudDNS: - description: Use the Google Cloud DNS API to manage DNS01 challenge records. - type: object - required: - - project - properties: - hostedZoneName: - description: HostedZoneName is an optional field that tells cert-manager in which Cloud DNS zone the challenge record has to be created. If left empty cert-manager will automatically choose a zone. - type: string - project: - type: string - serviceAccountSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - cloudflare: - description: Use the Cloudflare API to manage DNS01 challenge records. - type: object - properties: - apiKeySecretRef: - description: 'API key to use to authenticate with Cloudflare. Note: using an API token to authenticate is now the recommended method as it allows greater control of permissions.' - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - apiTokenSecretRef: - description: API token used to authenticate with Cloudflare. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - email: - description: Email of the account, only required when using API key based authentication. - type: string - cnameStrategy: - description: CNAMEStrategy configures how the DNS01 provider should handle CNAME records when found in DNS zones. + keyAlgorithm: + description: 'Deprecated: keyAlgorithm field exists for historical compatibility reasons and should not be used. The algorithm is now hardcoded to HS256 in golang/x/crypto/acme.' type: string enum: - - None - - Follow - digitalocean: - description: Use the DigitalOcean DNS API to manage DNS01 challenge records. - type: object - required: - - tokenSecretRef - properties: - tokenSecretRef: - description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - rfc2136: - description: Use RFC2136 ("Dynamic Updates in the Domain Name System") (https://datatracker.ietf.org/doc/rfc2136/) to manage DNS01 challenge records. - type: object - required: - - nameserver - properties: - nameserver: - description: The IP address or hostname of an authoritative DNS server supporting RFC2136 in the form host:port. If the host is an IPv6 address it must be enclosed in square brackets (e.g [2001:db8::1]) ; port is optional. This field is required. - type: string - tsigAlgorithm: - description: 'The TSIG Algorithm configured in the DNS supporting RFC2136. Used only when ``tsigSecretSecretRef`` and ``tsigKeyName`` are defined. Supported values are (case-insensitive): ``HMACMD5`` (default), ``HMACSHA1``, ``HMACSHA256`` or ``HMACSHA512``.' - type: string - tsigKeyName: - description: The TSIG Key name configured in the DNS. If ``tsigSecretSecretRef`` is defined, this field is required. - type: string - tsigSecretSecretRef: - description: The name of the secret containing the TSIG value. If ``tsigKeyName`` is defined, this field is required. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - route53: - description: Use the AWS Route53 API to manage DNS01 challenge records. - type: object - required: - - region - properties: - accessKeyID: - description: 'The AccessKeyID is used for authentication. Cannot be set when SecretAccessKeyID is set. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' - type: string - accessKeyIDSecretRef: - description: 'The SecretAccessKey is used for authentication. If set, pull the AWS access key ID from a key within a Kubernetes Secret. Cannot be set when AccessKeyID is set. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - hostedZoneID: - description: If set, the provider will manage only this zone in Route53 and will not do an lookup using the route53:ListHostedZonesByName api call. - type: string - region: - description: Always set the region when using AccessKeyID and SecretAccessKey - type: string - role: - description: Role is a Role ARN which the Route53 provider will assume using either the explicit credentials AccessKeyID/SecretAccessKey or the inferred credentials from environment variables, shared credentials file or AWS Instance metadata - type: string - secretAccessKeySecretRef: - description: 'The SecretAccessKey is used for authentication. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - webhook: - description: Configure an external webhook based DNS01 challenge solver to manage DNS01 challenge records. + - HS256 + - HS384 + - HS512 + keyID: + description: keyID is the ID of the CA key that the External Account is bound to. + type: string + keySecretRef: + description: keySecretRef is a Secret Key Selector referencing a data item in a Kubernetes Secret which holds the symmetric MAC key of the External Account Binding. The `key` is the index string that is paired with the key data in the Secret and should not be confused with the key data itself, or indeed with the External Account Binding keyID above. The secret key stored in the Secret **must** be un-padded, base64 URL encoded data. type: object required: - - groupName - - solverName + - name properties: - config: - description: Additional configuration that should be passed to the webhook apiserver when challenges are processed. This can contain arbitrary JSON data. Secret values should not be specified in this stanza. If secret values are needed (e.g. credentials for a DNS service), you should use a SecretKeySelector to reference a Secret resource. For details on the schema of this field, consult the webhook provider implementation's documentation. - x-kubernetes-preserve-unknown-fields: true - groupName: - description: The API group name that should be used when POSTing ChallengePayload resources to the webhook apiserver. This should be the same as the GroupName specified in the webhook provider implementation. + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. type: string - solverName: - description: The name of the solver to use, as defined in the webhook provider implementation. This will typically be the name of the provider, e.g. 'cloudflare'. + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' type: string - http01: - description: Configures cert-manager to attempt to complete authorizations by performing the HTTP01 challenge flow. It is not possible to obtain certificates for wildcard domain names (e.g. `*.example.com`) using the HTTP01 challenge mechanism. + preferredChain: + description: 'PreferredChain is the chain to use if the ACME server outputs multiple. PreferredChain is no guarantee that this one gets delivered by the ACME endpoint. For example, for Let''s Encrypt''s DST crosssign you would use: "DST Root CA X3" or "ISRG Root X1" for the newer Let''s Encrypt root CA. This value picks the first certificate bundle in the ACME alternative chains that has a certificate with this value as its issuer''s CN' + type: string + maxLength: 64 + privateKeySecretRef: + description: PrivateKey is the name of a Kubernetes Secret resource that will be used to store the automatically generated ACME account private key. Optionally, a `key` may be specified to select a specific entry within the named Secret resource. If `key` is not specified, a default of `tls.key` will be used. type: object + required: + - name properties: - gatewayHTTPRoute: - description: The Gateway API is a sig-network community API that models service networking in Kubernetes (https://gateway-api.sigs.k8s.io/). The Gateway solver will create HTTPRoutes with the specified labels in the same namespace as the challenge. This solver is experimental, and fields / behaviour may change in the future. - type: object - properties: - labels: - description: Custom labels that will be applied to HTTPRoutes created by cert-manager while solving HTTP-01 challenges. - type: object - additionalProperties: - type: string - parentRefs: - description: 'When solving an HTTP-01 challenge, cert-manager creates an HTTPRoute. cert-manager needs to know which parentRefs should be used when creating the HTTPRoute. Usually, the parentRef references a Gateway. See: https://gateway-api.sigs.k8s.io/api-types/httproute/#attaching-to-gateways' - type: array - items: - description: "ParentReference identifies an API object (usually a Gateway) that can be considered a parent of this resource (usually a route). The only kind of parent resource with \"Core\" support is Gateway. This API may be extended in the future to support additional kinds of parent resources, such as HTTPRoute. \n The API object must be valid in the cluster; the Group and Kind must be registered in the cluster for this reference to be valid." + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + server: + description: 'Server is the URL used to access the ACME server''s ''directory'' endpoint. For example, for Let''s Encrypt''s staging endpoint, you would use: "https://acme-staging-v02.api.letsencrypt.org/directory". Only ACME v2 endpoints (i.e. RFC 8555) are supported.' + type: string + skipTLSVerify: + description: 'INSECURE: Enables or disables validation of the ACME server TLS certificate. If true, requests to the ACME server will not have the TLS certificate chain validated. Mutually exclusive with CABundle; prefer using CABundle to prevent various kinds of security vulnerabilities. Only enable this option in development environments. If CABundle and SkipTLSVerify are unset, the system certificate bundle inside the container is used to validate the TLS connection. Defaults to false.' + type: boolean + solvers: + description: 'Solvers is a list of challenge solvers that will be used to solve ACME challenges for the matching domains. Solver configurations must be provided in order to obtain certificates from an ACME server. For more information, see: https://cert-manager.io/docs/configuration/acme/' + type: array + items: + description: An ACMEChallengeSolver describes how to solve ACME challenges for the issuer it is part of. A selector may be provided to use different solving strategies for different DNS names. Only one of HTTP01 or DNS01 must be provided. + type: object + properties: + dns01: + description: Configures cert-manager to attempt to complete authorizations by performing the DNS01 challenge flow. + type: object + properties: + acmeDNS: + description: Use the 'ACME DNS' (https://github.com/joohoi/acme-dns) API to manage DNS01 challenge records. + type: object + required: + - accountSecretRef + - host + properties: + accountSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + host: + type: string + akamai: + description: Use the Akamai DNS zone management API to manage DNS01 challenge records. + type: object + required: + - accessTokenSecretRef + - clientSecretSecretRef + - clientTokenSecretRef + - serviceConsumerDomain + properties: + accessTokenSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + clientSecretSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + clientTokenSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + serviceConsumerDomain: + type: string + azureDNS: + description: Use the Microsoft Azure DNS API to manage DNS01 challenge records. + type: object + required: + - resourceGroupName + - subscriptionID + properties: + clientID: + description: if both this and ClientSecret are left unset MSI will be used + type: string + clientSecretSecretRef: + description: if both this and ClientID are left unset MSI will be used + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + environment: + description: name of the Azure environment (default AzurePublicCloud) + type: string + enum: + - AzurePublicCloud + - AzureChinaCloud + - AzureGermanCloud + - AzureUSGovernmentCloud + hostedZoneName: + description: name of the DNS zone that should be used + type: string + managedIdentity: + description: managed identity configuration, can not be used at the same time as clientID, clientSecretSecretRef or tenantID + type: object + properties: + clientID: + description: client ID of the managed identity, can not be used at the same time as resourceID + type: string + resourceID: + description: resource ID of the managed identity, can not be used at the same time as clientID + type: string + resourceGroupName: + description: resource group the DNS zone is located in + type: string + subscriptionID: + description: ID of the Azure subscription + type: string + tenantID: + description: when specifying ClientID and ClientSecret then this field is also needed + type: string + cloudDNS: + description: Use the Google Cloud DNS API to manage DNS01 challenge records. type: object required: - - name + - project properties: - group: - description: "Group is the group of the referent. When unspecified, \"gateway.networking.k8s.io\" is inferred. To set the core API group (such as for a \"Service\" kind referent), Group must be explicitly set to \"\" (empty string). \n Support: Core" + hostedZoneName: + description: HostedZoneName is an optional field that tells cert-manager in which Cloud DNS zone the challenge record has to be created. If left empty cert-manager will automatically choose a zone. type: string - default: gateway.networking.k8s.io - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - kind: - description: "Kind is kind of the referent. \n Support: Core (Gateway) \n Support: Implementation-specific (Other Resources)" + project: type: string - default: Gateway - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - name: - description: "Name is the name of the referent. \n Support: Core" + serviceAccountSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + cloudflare: + description: Use the Cloudflare API to manage DNS01 challenge records. + type: object + properties: + apiKeySecretRef: + description: 'API key to use to authenticate with Cloudflare. Note: using an API token to authenticate is now the recommended method as it allows greater control of permissions.' + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + apiTokenSecretRef: + description: API token used to authenticate with Cloudflare. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + email: + description: Email of the account, only required when using API key based authentication. type: string - maxLength: 253 - minLength: 1 - namespace: - description: "Namespace is the namespace of the referent. When unspecified, this refers to the local namespace of the Route. \n Note that there are specific rules for ParentRefs which cross namespace boundaries. Cross-namespace references are only valid if they are explicitly allowed by something in the namespace they are referring to. For example: Gateway has the AllowedRoutes field, and ReferenceGrant provides a generic way to enable any other kind of cross-namespace reference. \n Support: Core" + cnameStrategy: + description: CNAMEStrategy configures how the DNS01 provider should handle CNAME records when found in DNS zones. + type: string + enum: + - None + - Follow + digitalocean: + description: Use the DigitalOcean DNS API to manage DNS01 challenge records. + type: object + required: + - tokenSecretRef + properties: + tokenSecretRef: + description: A reference to a specific 'key' within a Secret resource. In some instances, `key` is a required field. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + rfc2136: + description: Use RFC2136 ("Dynamic Updates in the Domain Name System") (https://datatracker.ietf.org/doc/rfc2136/) to manage DNS01 challenge records. + type: object + required: + - nameserver + properties: + nameserver: + description: The IP address or hostname of an authoritative DNS server supporting RFC2136 in the form host:port. If the host is an IPv6 address it must be enclosed in square brackets (e.g [2001:db8::1]) ; port is optional. This field is required. type: string - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - port: - description: "Port is the network port this Route targets. It can be interpreted differently based on the type of parent resource. \n When the parent resource is a Gateway, this targets all listeners listening on the specified port that also support this kind of Route(and select this Route). It's not recommended to set `Port` unless the networking behaviors specified in a Route must apply to a specific port as opposed to a listener(s) whose port(s) may be changed. When both Port and SectionName are specified, the name and port of the selected listener must match both specified values. \n Implementations MAY choose to support other parent resources. Implementations supporting other types of parent resources MUST clearly document how/if Port is interpreted. \n For the purpose of status, an attachment is considered successful as long as the parent resource accepts it partially. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Extended \n " - type: integer - format: int32 - maximum: 65535 - minimum: 1 - sectionName: - description: "SectionName is the name of a section within the target resource. In the following resources, SectionName is interpreted as the following: \n * Gateway: Listener Name. When both Port (experimental) and SectionName are specified, the name and port of the selected listener must match both specified values. \n Implementations MAY choose to support attaching Routes to other resources. If that is the case, they MUST clearly document how SectionName is interpreted. \n When unspecified (empty string), this will reference the entire resource. For the purpose of status, an attachment is considered successful if at least one section in the parent resource accepts it. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Core" + tsigAlgorithm: + description: 'The TSIG Algorithm configured in the DNS supporting RFC2136. Used only when ``tsigSecretSecretRef`` and ``tsigKeyName`` are defined. Supported values are (case-insensitive): ``HMACMD5`` (default), ``HMACSHA1``, ``HMACSHA256`` or ``HMACSHA512``.' type: string - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - serviceType: - description: Optional service type for Kubernetes solver service. Supported values are NodePort or ClusterIP. If unset, defaults to NodePort. - type: string - ingress: - description: The ingress based HTTP01 challenge solver will solve challenges by creating or modifying Ingress resources in order to route requests for '/.well-known/acme-challenge/XYZ' to 'challenge solver' pods that are provisioned by cert-manager for each Challenge to be completed. - type: object - properties: - class: - description: The ingress class to use when creating Ingress resources to solve ACME challenges that use this challenge solver. Only one of 'class' or 'name' may be specified. - type: string - ingressTemplate: - description: Optional ingress template used to configure the ACME challenge solver ingress used for HTTP01 challenges. - type: object - properties: - metadata: - description: ObjectMeta overrides for the ingress used to solve HTTP01 challenges. Only the 'labels' and 'annotations' fields may be set. If labels or annotations overlap with in-built values, the values here will override the in-built values. - type: object - properties: - annotations: - description: Annotations that should be added to the created ACME HTTP01 solver ingress. - type: object - additionalProperties: + tsigKeyName: + description: The TSIG Key name configured in the DNS. If ``tsigSecretSecretRef`` is defined, this field is required. + type: string + tsigSecretSecretRef: + description: The name of the secret containing the TSIG value. If ``tsigKeyName`` is defined, this field is required. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. type: string - labels: - description: Labels that should be added to the created ACME HTTP01 solver ingress. - type: object - additionalProperties: + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' type: string - name: - description: The name of the ingress resource that should have ACME challenge solving routes inserted into it in order to solve HTTP01 challenges. This is typically used in conjunction with ingress controllers like ingress-gce, which maintains a 1:1 mapping between external IPs and ingress resources. - type: string - podTemplate: - description: Optional pod template used to configure the ACME challenge solver pods used for HTTP01 challenges. - type: object - properties: - metadata: - description: ObjectMeta overrides for the pod used to solve HTTP01 challenges. Only the 'labels' and 'annotations' fields may be set. If labels or annotations overlap with in-built values, the values here will override the in-built values. - type: object - properties: - annotations: - description: Annotations that should be added to the create ACME HTTP01 solver pods. - type: object - additionalProperties: + route53: + description: Use the AWS Route53 API to manage DNS01 challenge records. + type: object + required: + - region + properties: + accessKeyID: + description: 'The AccessKeyID is used for authentication. Cannot be set when SecretAccessKeyID is set. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' + type: string + accessKeyIDSecretRef: + description: 'The SecretAccessKey is used for authentication. If set, pull the AWS access key ID from a key within a Kubernetes Secret. Cannot be set when AccessKeyID is set. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. type: string - labels: - description: Labels that should be added to the created ACME HTTP01 solver pods. - type: object - additionalProperties: + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' type: string - spec: - description: PodSpec defines overrides for the HTTP01 challenge solver pod. Only the 'priorityClassName', 'nodeSelector', 'affinity', 'serviceAccountName' and 'tolerations' fields are supported currently. All other fields will be ignored. - type: object - properties: - affinity: - description: If specified, the pod's scheduling constraints + hostedZoneID: + description: If set, the provider will manage only this zone in Route53 and will not do an lookup using the route53:ListHostedZonesByName api call. + type: string + region: + description: Always set the region when using AccessKeyID and SecretAccessKey + type: string + role: + description: Role is a Role ARN which the Route53 provider will assume using either the explicit credentials AccessKeyID/SecretAccessKey or the inferred credentials from environment variables, shared credentials file or AWS Instance metadata + type: string + secretAccessKeySecretRef: + description: 'The SecretAccessKey is used for authentication. If neither the Access Key nor Key ID are set, we fall-back to using env vars, shared credentials file or AWS Instance metadata, see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials' + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + webhook: + description: Configure an external webhook based DNS01 challenge solver to manage DNS01 challenge records. + type: object + required: + - groupName + - solverName + properties: + config: + description: Additional configuration that should be passed to the webhook apiserver when challenges are processed. This can contain arbitrary JSON data. Secret values should not be specified in this stanza. If secret values are needed (e.g. credentials for a DNS service), you should use a SecretKeySelector to reference a Secret resource. For details on the schema of this field, consult the webhook provider implementation's documentation. + x-kubernetes-preserve-unknown-fields: true + groupName: + description: The API group name that should be used when POSTing ChallengePayload resources to the webhook apiserver. This should be the same as the GroupName specified in the webhook provider implementation. + type: string + solverName: + description: The name of the solver to use, as defined in the webhook provider implementation. This will typically be the name of the provider, e.g. 'cloudflare'. + type: string + http01: + description: Configures cert-manager to attempt to complete authorizations by performing the HTTP01 challenge flow. It is not possible to obtain certificates for wildcard domain names (e.g. `*.example.com`) using the HTTP01 challenge mechanism. + type: object + properties: + gatewayHTTPRoute: + description: The Gateway API is a sig-network community API that models service networking in Kubernetes (https://gateway-api.sigs.k8s.io/). The Gateway solver will create HTTPRoutes with the specified labels in the same namespace as the challenge. This solver is experimental, and fields / behaviour may change in the future. + type: object + properties: + labels: + description: Custom labels that will be applied to HTTPRoutes created by cert-manager while solving HTTP-01 challenges. + type: object + additionalProperties: + type: string + parentRefs: + description: 'When solving an HTTP-01 challenge, cert-manager creates an HTTPRoute. cert-manager needs to know which parentRefs should be used when creating the HTTPRoute. Usually, the parentRef references a Gateway. See: https://gateway-api.sigs.k8s.io/api-types/httproute/#attaching-to-gateways' + type: array + items: + description: "ParentReference identifies an API object (usually a Gateway) that can be considered a parent of this resource (usually a route). There are two kinds of parent resources with \"Core\" support: \n * Gateway (Gateway conformance profile) * Service (Mesh conformance profile, experimental, ClusterIP Services only) \n This API may be extended in the future to support additional kinds of parent resources. \n The API object must be valid in the cluster; the Group and Kind must be registered in the cluster for this reference to be valid." type: object + required: + - name properties: - nodeAffinity: - description: Describes node affinity scheduling rules for the pod. - type: object - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node matches the corresponding matchExpressions; the node(s) with the highest sum are the most preferred. - type: array - items: - description: An empty preferred scheduling term matches all objects with implicit weight 0 (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + group: + description: "Group is the group of the referent. When unspecified, \"gateway.networking.k8s.io\" is inferred. To set the core API group (such as for a \"Service\" kind referent), Group must be explicitly set to \"\" (empty string). \n Support: Core" + type: string + default: gateway.networking.k8s.io + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + kind: + description: "Kind is kind of the referent. \n There are two kinds of parent resources with \"Core\" support: \n * Gateway (Gateway conformance profile) * Service (Mesh conformance profile, experimental, ClusterIP Services only) \n Support for other resources is Implementation-Specific." + type: string + default: Gateway + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + name: + description: "Name is the name of the referent. \n Support: Core" + type: string + maxLength: 253 + minLength: 1 + namespace: + description: "Namespace is the namespace of the referent. When unspecified, this refers to the local namespace of the Route. \n Note that there are specific rules for ParentRefs which cross namespace boundaries. Cross-namespace references are only valid if they are explicitly allowed by something in the namespace they are referring to. For example: Gateway has the AllowedRoutes field, and ReferenceGrant provides a generic way to enable any other kind of cross-namespace reference. \n ParentRefs from a Route to a Service in the same namespace are \"producer\" routes, which apply default routing rules to inbound connections from any namespace to the Service. \n ParentRefs from a Route to a Service in a different namespace are \"consumer\" routes, and these routing rules are only applied to outbound connections originating from the same namespace as the Route, for which the intended destination of the connections are a Service targeted as a ParentRef of the Route. \n Support: Core" + type: string + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + port: + description: "Port is the network port this Route targets. It can be interpreted differently based on the type of parent resource. \n When the parent resource is a Gateway, this targets all listeners listening on the specified port that also support this kind of Route(and select this Route). It's not recommended to set `Port` unless the networking behaviors specified in a Route must apply to a specific port as opposed to a listener(s) whose port(s) may be changed. When both Port and SectionName are specified, the name and port of the selected listener must match both specified values. \n When the parent resource is a Service, this targets a specific port in the Service spec. When both Port (experimental) and SectionName are specified, the name and port of the selected port must match both specified values. \n Implementations MAY choose to support other parent resources. Implementations supporting other types of parent resources MUST clearly document how/if Port is interpreted. \n For the purpose of status, an attachment is considered successful as long as the parent resource accepts it partially. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Extended \n " + type: integer + format: int32 + maximum: 65535 + minimum: 1 + sectionName: + description: "SectionName is the name of a section within the target resource. In the following resources, SectionName is interpreted as the following: \n * Gateway: Listener Name. When both Port (experimental) and SectionName are specified, the name and port of the selected listener must match both specified values. * Service: Port Name. When both Port (experimental) and SectionName are specified, the name and port of the selected listener must match both specified values. Note that attaching Routes to Services as Parents is part of experimental Mesh support and is not supported for any other purpose. \n Implementations MAY choose to support attaching Routes to other resources. If that is the case, they MUST clearly document how SectionName is interpreted. \n When unspecified (empty string), this will reference the entire resource. For the purpose of status, an attachment is considered successful if at least one section in the parent resource accepts it. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Core" + type: string + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + serviceType: + description: Optional service type for Kubernetes solver service. Supported values are NodePort or ClusterIP. If unset, defaults to NodePort. + type: string + ingress: + description: The ingress based HTTP01 challenge solver will solve challenges by creating or modifying Ingress resources in order to route requests for '/.well-known/acme-challenge/XYZ' to 'challenge solver' pods that are provisioned by cert-manager for each Challenge to be completed. + type: object + properties: + class: + description: This field configures the annotation `kubernetes.io/ingress.class` when creating Ingress resources to solve ACME challenges that use this challenge solver. Only one of `class`, `name` or `ingressClassName` may be specified. + type: string + ingressClassName: + description: This field configures the field `ingressClassName` on the created Ingress resources used to solve ACME challenges that use this challenge solver. This is the recommended way of configuring the ingress class. Only one of `class`, `name` or `ingressClassName` may be specified. + type: string + ingressTemplate: + description: Optional ingress template used to configure the ACME challenge solver ingress used for HTTP01 challenges. + type: object + properties: + metadata: + description: ObjectMeta overrides for the ingress used to solve HTTP01 challenges. Only the 'labels' and 'annotations' fields may be set. If labels or annotations overlap with in-built values, the values here will override the in-built values. + type: object + properties: + annotations: + description: Annotations that should be added to the created ACME HTTP01 solver ingress. + type: object + additionalProperties: + type: string + labels: + description: Labels that should be added to the created ACME HTTP01 solver ingress. + type: object + additionalProperties: + type: string + name: + description: The name of the ingress resource that should have ACME challenge solving routes inserted into it in order to solve HTTP01 challenges. This is typically used in conjunction with ingress controllers like ingress-gce, which maintains a 1:1 mapping between external IPs and ingress resources. Only one of `class`, `name` or `ingressClassName` may be specified. + type: string + podTemplate: + description: Optional pod template used to configure the ACME challenge solver pods used for HTTP01 challenges. + type: object + properties: + metadata: + description: ObjectMeta overrides for the pod used to solve HTTP01 challenges. Only the 'labels' and 'annotations' fields may be set. If labels or annotations overlap with in-built values, the values here will override the in-built values. + type: object + properties: + annotations: + description: Annotations that should be added to the create ACME HTTP01 solver pods. + type: object + additionalProperties: + type: string + labels: + description: Labels that should be added to the created ACME HTTP01 solver pods. + type: object + additionalProperties: + type: string + spec: + description: PodSpec defines overrides for the HTTP01 challenge solver pod. Check ACMEChallengeSolverHTTP01IngressPodSpec to find out currently supported fields. All other fields will be ignored. + type: object + properties: + affinity: + description: If specified, the pod's scheduling constraints + type: object + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the pod. type: object - required: - - preference - - weight properties: - preference: - description: A node selector term, associated with the corresponding weight. - type: object - properties: - matchExpressions: - description: A list of node selector requirements by node's labels. - type: array - items: - description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node matches the corresponding matchExpressions; the node(s) with the highest sum are the most preferred. + type: array + items: + description: An empty preferred scheduling term matches all objects with implicit weight 0 (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + type: object + required: + - preference + - weight + properties: + preference: + description: A node selector term, associated with the corresponding weight. type: object - required: - - key - - operator properties: - key: - description: The label key that the selector applies to. - type: string - operator: - description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. + matchExpressions: + description: A list of node selector requirements by node's labels. type: array items: - type: string - matchFields: - description: A list of node selector requirements by node's fields. - type: array - items: - description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: The label key that the selector applies to. - type: string - operator: - description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. + description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: The label key that the selector applies to. + type: string + operator: + description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchFields: + description: A list of node selector requirements by node's fields. type: array items: - type: string - x-kubernetes-map-type: atomic - weight: - description: Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100. - type: integer - format: int32 - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to an update), the system may or may not try to eventually evict the pod from its node. - type: object - required: - - nodeSelectorTerms - properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. The terms are ORed. - type: array - items: - description: A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: The label key that the selector applies to. + type: string + operator: + description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. + type: array + items: + type: string + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100. + type: integer + format: int32 + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to an update), the system may or may not try to eventually evict the pod from its node. type: object + required: + - nodeSelectorTerms properties: - matchExpressions: - description: A list of node selector requirements by node's labels. - type: array - items: - description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: The label key that the selector applies to. - type: string - operator: - description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. - type: array - items: - type: string - matchFields: - description: A list of node selector requirements by node's fields. + nodeSelectorTerms: + description: Required. A list of node selector terms. The terms are ORed. type: array items: - description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + description: A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. type: object - required: - - key - - operator properties: - key: - description: The label key that the selector applies to. - type: string - operator: - description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. - type: string - values: - description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. + matchExpressions: + description: A list of node selector requirements by node's labels. type: array items: - type: string + description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: The label key that the selector applies to. + type: string + operator: + description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchFields: + description: A list of node selector requirements by node's fields. + type: array + items: + description: A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: The label key that the selector applies to. + type: string + operator: + description: Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch. + type: array + items: + type: string + x-kubernetes-map-type: atomic x-kubernetes-map-type: atomic - x-kubernetes-map-type: atomic - podAffinity: - description: Describes pod affinity scheduling rules (e.g. co-locate this pod in the same node, zone, etc. as some other pod(s)). - type: object - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. - type: array - items: - description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s) + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate this pod in the same node, zone, etc. as some other pod(s)). type: object - required: - - podAffinityTerm - - weight properties: - podAffinityTerm: - description: Required. A pod affinity term, associated with the corresponding weight. - type: object - required: - - topologyKey - properties: - labelSelector: - description: A label query over a set of resources, in this case pods. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. + type: array + items: + description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s) + type: object + required: + - podAffinityTerm + - weight + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated with the corresponding weight. + type: object + required: + - topologyKey + properties: + labelSelector: + description: A label query over a set of resources, in this case pods. type: object - required: - - key - - operator properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. type: array items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: - type: string - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. type: object - required: - - key - - operator properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. type: array items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: - type: string - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". - type: array - items: - type: string - topologyKey: - description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. - type: string - weight: - description: weight associated with matching the corresponding podAffinityTerm, in the range 1-100. - type: integer - format: int32 - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied. - type: array - items: - description: Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running - type: object - required: - - topologyKey - properties: - labelSelector: - description: A label query over a set of resources, in this case pods. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - type: object - required: - - key - - operator - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". + type: array + items: + type: string + topologyKey: + description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + weight: + description: weight associated with matching the corresponding podAffinityTerm, in the range 1-100. + type: integer + format: int32 + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied. + type: array + items: + description: Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running + type: object + required: + - topologyKey + properties: + labelSelector: + description: A label query over a set of resources, in this case pods. + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. type: array items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: - type: string - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. type: object - required: - - key - - operator properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. type: array items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". + type: array + items: + type: string + topologyKey: + description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. type: string - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". - type: array - items: - type: string - topologyKey: - description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. - type: string - podAntiAffinity: - description: Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s)). - type: object - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to nodes that satisfy the anti-affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling anti-affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. - type: array - items: - description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s) + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s)). type: object - required: - - podAffinityTerm - - weight properties: - podAffinityTerm: - description: Required. A pod affinity term, associated with the corresponding weight. - type: object - required: - - topologyKey - properties: - labelSelector: - description: A label query over a set of resources, in this case pods. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes that satisfy the anti-affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling anti-affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred. + type: array + items: + description: The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s) + type: object + required: + - podAffinityTerm + - weight + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated with the corresponding weight. + type: object + required: + - topologyKey + properties: + labelSelector: + description: A label query over a set of resources, in this case pods. type: object - required: - - key - - operator properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. type: array items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: - type: string - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. type: object - required: - - key - - operator properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. type: array items: - type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: - type: string - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". - type: array - items: - type: string - topologyKey: - description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. - type: string - weight: - description: weight associated with matching the corresponding podAffinityTerm, in the range 1-100. - type: integer - format: int32 - requiredDuringSchedulingIgnoredDuringExecution: - description: If the anti-affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the anti-affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied. - type: array - items: - description: Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running - type: object - required: - - topologyKey - properties: - labelSelector: - description: A label query over a set of resources, in this case pods. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: + type: string + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". + type: array + items: + type: string + topologyKey: + description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. + type: string + weight: + description: weight associated with matching the corresponding podAffinityTerm, in the range 1-100. + type: integer + format: int32 + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the anti-affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied. + type: array + items: + description: Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running + type: object + required: + - topologyKey + properties: + labelSelector: + description: A label query over a set of resources, in this case pods. type: object - required: - - key - - operator properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. type: array items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: - type: string - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. - type: object - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - type: array - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + x-kubernetes-map-type: atomic + namespaceSelector: + description: A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means "this pod's namespace". An empty selector ({}) matches all namespaces. type: object - required: - - key - - operator properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. type: array items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + additionalProperties: type: string - matchLabels: - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - additionalProperties: + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". + type: array + items: + type: string + topologyKey: + description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. type: string - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means "this pod's namespace". - type: array - items: - type: string - topologyKey: - description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. - type: string - nodeSelector: - description: 'NodeSelector is a selector which must be true for the pod to fit on a node. Selector which must match a node''s labels for the pod to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/' - type: object - additionalProperties: - type: string - priorityClassName: - description: If specified, the pod's priorityClassName. - type: string - serviceAccountName: - description: If specified, the pod's service account - type: string - tolerations: - description: If specified, the pod's tolerations. - type: array - items: - description: The pod this Toleration is attached to tolerates any taint that matches the triple using the matching operator . - type: object - properties: - effect: - description: Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys. - type: string - operator: - description: Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category. + imagePullSecrets: + description: If specified, the pod's imagePullSecrets + type: array + items: + description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. + type: object + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + x-kubernetes-map-type: atomic + nodeSelector: + description: 'NodeSelector is a selector which must be true for the pod to fit on a node. Selector which must match a node''s labels for the pod to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/' + type: object + additionalProperties: + type: string + priorityClassName: + description: If specified, the pod's priorityClassName. type: string - tolerationSeconds: - description: TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system. - type: integer - format: int64 - value: - description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string. + serviceAccountName: + description: If specified, the pod's service account type: string - serviceType: - description: Optional service type for Kubernetes solver service. Supported values are NodePort or ClusterIP. If unset, defaults to NodePort. - type: string - selector: - description: Selector selects a set of DNSNames on the Certificate resource that should be solved using this challenge solver. If not specified, the solver will be treated as the 'default' solver with the lowest priority, i.e. if any other solver has a more specific match, it will be used instead. + tolerations: + description: If specified, the pod's tolerations. + type: array + items: + description: The pod this Toleration is attached to tolerates any taint that matches the triple using the matching operator . + type: object + properties: + effect: + description: Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system. + type: integer + format: int64 + value: + description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + serviceType: + description: Optional service type for Kubernetes solver service. Supported values are NodePort or ClusterIP. If unset, defaults to NodePort. + type: string + selector: + description: Selector selects a set of DNSNames on the Certificate resource that should be solved using this challenge solver. If not specified, the solver will be treated as the 'default' solver with the lowest priority, i.e. if any other solver has a more specific match, it will be used instead. + type: object + properties: + dnsNames: + description: List of DNSNames that this solver will be used to solve. If specified and a match is found, a dnsNames selector will take precedence over a dnsZones selector. If multiple solvers match with the same dnsNames value, the solver with the most matching labels in matchLabels will be selected. If neither has more matches, the solver defined earlier in the list will be selected. + type: array + items: + type: string + dnsZones: + description: List of DNSZones that this solver will be used to solve. The most specific DNS zone match specified here will take precedence over other DNS zone matches, so a solver specifying sys.example.com will be selected over one specifying example.com for the domain www.sys.example.com. If multiple solvers match with the same dnsZones value, the solver with the most matching labels in matchLabels will be selected. If neither has more matches, the solver defined earlier in the list will be selected. + type: array + items: + type: string + matchLabels: + description: A label selector that is used to refine the set of certificate's that this challenge solver will apply to. + type: object + additionalProperties: + type: string + ca: + description: CA configures this issuer to sign certificates using a signing CA keypair stored in a Secret resource. This is used to build internal PKIs that are managed by cert-manager. + type: object + required: + - secretName + properties: + crlDistributionPoints: + description: The CRL distribution points is an X.509 v3 certificate extension which identifies the location of the CRL from which the revocation of this certificate can be checked. If not set, certificates will be issued without distribution points set. + type: array + items: + type: string + ocspServers: + description: The OCSP server list is an X.509 v3 extension that defines a list of URLs of OCSP responders. The OCSP responders can be queried for the revocation status of an issued certificate. If not set, the certificate will be issued with no OCSP servers set. For example, an OCSP server URL could be "http://ocsp.int-x3.letsencrypt.org". + type: array + items: + type: string + secretName: + description: SecretName is the name of the secret used to sign Certificates issued by this Issuer. + type: string + selfSigned: + description: SelfSigned configures this issuer to 'self sign' certificates using the private key used to create the CertificateRequest object. + type: object + properties: + crlDistributionPoints: + description: The CRL distribution points is an X.509 v3 certificate extension which identifies the location of the CRL from which the revocation of this certificate can be checked. If not set certificate will be issued without CDP. Values are strings. + type: array + items: + type: string + vault: + description: Vault configures this issuer to sign certificates using a HashiCorp Vault PKI backend. + type: object + required: + - auth + - path + - server + properties: + auth: + description: Auth configures how cert-manager authenticates with the Vault server. type: object properties: - dnsNames: - description: List of DNSNames that this solver will be used to solve. If specified and a match is found, a dnsNames selector will take precedence over a dnsZones selector. If multiple solvers match with the same dnsNames value, the solver with the most matching labels in matchLabels will be selected. If neither has more matches, the solver defined earlier in the list will be selected. - type: array - items: - type: string - dnsZones: - description: List of DNSZones that this solver will be used to solve. The most specific DNS zone match specified here will take precedence over other DNS zone matches, so a solver specifying sys.example.com will be selected over one specifying example.com for the domain www.sys.example.com. If multiple solvers match with the same dnsZones value, the solver with the most matching labels in matchLabels will be selected. If neither has more matches, the solver defined earlier in the list will be selected. - type: array - items: - type: string - matchLabels: - description: A label selector that is used to refine the set of certificate's that this challenge solver will apply to. + appRole: + description: AppRole authenticates with Vault using the App Role auth mechanism, with the role and secret stored in a Kubernetes Secret resource. type: object - additionalProperties: - type: string - token: - description: The ACME challenge token for this challenge. This is the raw value returned from the ACME server. - type: string - type: - description: The type of ACME challenge this resource represents. One of "HTTP-01" or "DNS-01". - type: string - enum: - - HTTP-01 - - DNS-01 - url: - description: The URL of the ACME Challenge resource for this challenge. This can be used to lookup details about the status of this challenge. - type: string - wildcard: - description: wildcard will be true if this challenge is for a wildcard identifier, for example '*.example.com'. - type: boolean - status: - type: object - properties: - presented: - description: presented will be set to true if the challenge values for this challenge are currently 'presented'. This *does not* imply the self check is passing. Only that the values have been 'submitted' for the appropriate challenge mechanism (i.e. the DNS01 TXT record has been presented, or the HTTP01 configuration has been configured). - type: boolean - processing: - description: Used to denote whether this challenge should be processed or not. This field will only be set to true by the 'scheduling' component. It will only be set to false by the 'challenges' controller, after the challenge has reached a final state or timed out. If this field is set to false, the challenge controller will not take any more action. - type: boolean - reason: - description: Contains human readable information on why the Challenge is in the current state. - type: string - state: - description: Contains the current 'state' of the challenge. If not set, the state of the challenge is unknown. - type: string - enum: - - valid - - ready - - pending - - processing - - invalid - - expired - - errored - served: true - storage: true - subresources: - status: {} ---- -# Source: cert-manager/templates/crds.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificaterequests.cert-manager.io - labels: - app: 'cert-manager' - app.kubernetes.io/name: 'cert-manager' - app.kubernetes.io/instance: 'cert-manager' - # Generated labels - app.kubernetes.io/version: "v1.11.0" -spec: - group: cert-manager.io - names: - kind: CertificateRequest - listKind: CertificateRequestList - plural: certificaterequests - shortNames: - - cr - - crs - singular: certificaterequest - categories: - - cert-manager - scope: Namespaced - versions: - - name: v1 - subresources: - status: {} - additionalPrinterColumns: - - jsonPath: .status.conditions[?(@.type=="Approved")].status - name: Approved - type: string - - jsonPath: .status.conditions[?(@.type=="Denied")].status - name: Denied - type: string - - jsonPath: .status.conditions[?(@.type=="Ready")].status - name: Ready - type: string - - jsonPath: .spec.issuerRef.name - name: Issuer - type: string - - jsonPath: .spec.username - name: Requestor - type: string - - jsonPath: .status.conditions[?(@.type=="Ready")].message - name: Status - priority: 1 - type: string - - jsonPath: .metadata.creationTimestamp - description: CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. - name: Age - type: date - schema: - openAPIV3Schema: - description: "A CertificateRequest is used to request a signed certificate from one of the configured issuers. \n All fields within the CertificateRequest's `spec` are immutable after creation. A CertificateRequest will either succeed or fail, as denoted by its `status.state` field. \n A CertificateRequest is a one-shot resource, meaning it represents a single point in time request for a certificate and cannot be re-used." - type: object - required: - - spec - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: Desired state of the CertificateRequest resource. - type: object - required: - - issuerRef - - request - properties: - duration: - description: The requested 'duration' (i.e. lifetime) of the Certificate. This option may be ignored/overridden by some issuer types. - type: string - extra: - description: Extra contains extra attributes of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable. - type: object - additionalProperties: - type: array - items: + required: + - path + - roleId + - secretRef + properties: + path: + description: 'Path where the App Role authentication backend is mounted in Vault, e.g: "approle"' + type: string + roleId: + description: RoleID configured in the App Role authentication backend when setting up the authentication backend in Vault. + type: string + secretRef: + description: Reference to a key in a Secret that contains the App Role secret used to authenticate with Vault. The `key` field must be specified and denotes which entry within the Secret resource is used as the app role secret. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + kubernetes: + description: Kubernetes authenticates with Vault by passing the ServiceAccount token stored in the named Secret resource to the Vault server. + type: object + required: + - role + properties: + mountPath: + description: The Vault mountPath here is the mount path to use when authenticating with Vault. For example, setting a value to `/v1/auth/foo`, will use the path `/v1/auth/foo/login` to authenticate with Vault. If unspecified, the default value "/v1/auth/kubernetes" will be used. + type: string + role: + description: A required field containing the Vault Role to assume. A Role binds a Kubernetes ServiceAccount with a set of Vault policies. + type: string + secretRef: + description: The required Secret field containing a Kubernetes ServiceAccount JWT used for authenticating with Vault. Use of 'ambient credentials' is not supported. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + serviceAccountRef: + description: A reference to a service account that will be used to request a bound token (also known as "projected token"). Compared to using "secretRef", using this field means that you don't rely on statically bound tokens. To use this field, you must configure an RBAC rule to let cert-manager request a token. + type: object + required: + - name + properties: + name: + description: Name of the ServiceAccount used to request a token. + type: string + tokenSecretRef: + description: TokenSecretRef authenticates with Vault by presenting a token. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + caBundle: + description: Base64-encoded bundle of PEM CAs which will be used to validate the certificate chain presented by Vault. Only used if using HTTPS to connect to Vault and ignored for HTTP connections. Mutually exclusive with CABundleSecretRef. If neither CABundle nor CABundleSecretRef are defined, the certificate bundle in the cert-manager controller container is used to validate the TLS connection. type: string - groups: - description: Groups contains group membership of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable. - type: array - items: - type: string - x-kubernetes-list-type: atomic - isCA: - description: IsCA will request to mark the certificate as valid for certificate signing when submitting to the issuer. This will automatically add the `cert sign` usage to the list of `usages`. - type: boolean - issuerRef: - description: IssuerRef is a reference to the issuer for this CertificateRequest. If the `kind` field is not set, or set to `Issuer`, an Issuer resource with the given name in the same namespace as the CertificateRequest will be used. If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the provided name will be used. The `name` field in this stanza is required at all times. The group field refers to the API group of the issuer which defaults to `cert-manager.io` if empty. + format: byte + caBundleSecretRef: + description: Reference to a Secret containing a bundle of PEM-encoded CAs to use when verifying the certificate chain presented by Vault when using HTTPS. Mutually exclusive with CABundle. If neither CABundle nor CABundleSecretRef are defined, the certificate bundle in the cert-manager controller container is used to validate the TLS connection. If no key for the Secret is specified, cert-manager will default to 'ca.crt'. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Name of the vault namespace. Namespaces is a set of features within Vault Enterprise that allows Vault environments to support Secure Multi-tenancy. e.g: "ns1" More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces' + type: string + path: + description: 'Path is the mount path of the Vault PKI backend''s `sign` endpoint, e.g: "my_pki_mount/sign/my-role-name".' + type: string + server: + description: 'Server is the connection address for the Vault server, e.g: "https://vault.example.com:8200".' + type: string + venafi: + description: Venafi configures this issuer to sign certificates using a Venafi TPP or Venafi Cloud policy zone. type: object required: - - name + - zone properties: - group: - description: Group of the resource being referred to. - type: string - kind: - description: Kind of the resource being referred to. - type: string - name: - description: Name of the resource being referred to. + cloud: + description: Cloud specifies the Venafi cloud configuration settings. Only one of TPP or Cloud may be specified. + type: object + required: + - apiTokenSecretRef + properties: + apiTokenSecretRef: + description: APITokenSecretRef is a secret key selector for the Venafi Cloud API token. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + url: + description: URL is the base URL for Venafi Cloud. Defaults to "https://api.venafi.cloud/v1". + type: string + tpp: + description: TPP specifies Trust Protection Platform configuration settings. Only one of TPP or Cloud may be specified. + type: object + required: + - credentialsRef + - url + properties: + caBundle: + description: Base64-encoded bundle of PEM CAs which will be used to validate the certificate chain presented by the TPP server. Only used if using HTTPS; ignored for HTTP. If undefined, the certificate bundle in the cert-manager controller container is used to validate the chain. + type: string + format: byte + credentialsRef: + description: CredentialsRef is a reference to a Secret containing the username and password for the TPP server. The secret must contain two keys, 'username' and 'password'. + type: object + required: + - name + properties: + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + url: + description: 'URL is the base URL for the vedsdk endpoint of the Venafi TPP instance, for example: "https://tpp.example.com/vedsdk".' + type: string + zone: + description: Zone is the Venafi Policy Zone to use for this issuer. All requests made to the Venafi platform will be restricted by the named zone policy. This field is required. type: string - request: - description: The PEM-encoded x509 certificate signing request to be submitted to the CA for signing. - type: string - format: byte - uid: - description: UID contains the uid of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable. - type: string - usages: - description: Usages is the set of x509 usages that are requested for the certificate. If usages are set they SHOULD be encoded inside the CSR spec Defaults to `digital signature` and `key encipherment` if not specified. - type: array - items: - description: "KeyUsage specifies valid usage contexts for keys. See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3 https://tools.ietf.org/html/rfc5280#section-4.2.1.12 \n Valid KeyUsage values are as follows: \"signing\", \"digital signature\", \"content commitment\", \"key encipherment\", \"key agreement\", \"data encipherment\", \"cert sign\", \"crl sign\", \"encipher only\", \"decipher only\", \"any\", \"server auth\", \"client auth\", \"code signing\", \"email protection\", \"s/mime\", \"ipsec end system\", \"ipsec tunnel\", \"ipsec user\", \"timestamping\", \"ocsp signing\", \"microsoft sgc\", \"netscape sgc\"" - type: string - enum: - - signing - - digital signature - - content commitment - - key encipherment - - key agreement - - data encipherment - - cert sign - - crl sign - - encipher only - - decipher only - - any - - server auth - - client auth - - code signing - - email protection - - s/mime - - ipsec end system - - ipsec tunnel - - ipsec user - - timestamping - - ocsp signing - - microsoft sgc - - netscape sgc - username: - description: Username contains the name of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable. - type: string status: - description: Status of the CertificateRequest. This is set and managed automatically. + description: Status of the ClusterIssuer. This is set and managed automatically. type: object properties: - ca: - description: The PEM encoded x509 certificate of the signer, also known as the CA (Certificate Authority). This is set on a best-effort basis by different issuers. If not set, the CA is assumed to be unknown/not available. - type: string - format: byte - certificate: - description: The PEM encoded x509 certificate resulting from the certificate signing request. If not set, the CertificateRequest has either not been completed or has failed. More information on failure can be found by checking the `conditions` field. - type: string - format: byte + acme: + description: ACME specific status options. This field should only be set if the Issuer is configured to use an ACME server to issue certificates. + type: object + properties: + lastPrivateKeyHash: + description: LastPrivateKeyHash is a hash of the private key associated with the latest registered ACME account, in order to track changes made to registered account associated with the Issuer + type: string + lastRegisteredEmail: + description: LastRegisteredEmail is the email associated with the latest registered ACME account, in order to track changes made to registered account associated with the Issuer + type: string + uri: + description: URI is the unique account identifier, which can also be used to retrieve account details from the CA + type: string conditions: - description: List of status conditions to indicate the status of a CertificateRequest. Known condition types are `Ready` and `InvalidRequest`. + description: List of status conditions to indicate the status of a CertificateRequest. Known condition types are `Ready`. type: array items: - description: CertificateRequestCondition contains condition information for a CertificateRequest. + description: IssuerCondition contains condition information for an Issuer. type: object required: - status @@ -2548,6 +2956,10 @@ spec: message: description: Message is a human readable description of the details of the last transition, complementing reason. type: string + observedGeneration: + description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Issuer. + type: integer + format: int64 reason: description: Reason is a brief machine readable explanation for the condition's last transition. type: string @@ -2559,15 +2971,11 @@ spec: - "False" - Unknown type: - description: Type of the condition, known values are (`Ready`, `InvalidRequest`, `Approved`, `Denied`). + description: Type of the condition, known values are (`Ready`). type: string x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map - failureTime: - description: FailureTime stores the time that this CertificateRequest failed. This is used to influence garbage collection and back-off. - type: string - format: date-time served: true storage: true --- @@ -2579,9 +2987,9 @@ metadata: labels: app: 'cert-manager' app.kubernetes.io/name: 'cert-manager' - app.kubernetes.io/instance: 'cert-manager' + app.kubernetes.io/instance: "cert-manager" # Generated labels - app.kubernetes.io/version: "v1.11.0" + app.kubernetes.io/version: "v1.13.1" spec: group: cert-manager.io names: @@ -3009,7 +3417,7 @@ spec: description: 'When solving an HTTP-01 challenge, cert-manager creates an HTTPRoute. cert-manager needs to know which parentRefs should be used when creating the HTTPRoute. Usually, the parentRef references a Gateway. See: https://gateway-api.sigs.k8s.io/api-types/httproute/#attaching-to-gateways' type: array items: - description: "ParentReference identifies an API object (usually a Gateway) that can be considered a parent of this resource (usually a route). The only kind of parent resource with \"Core\" support is Gateway. This API may be extended in the future to support additional kinds of parent resources, such as HTTPRoute. \n The API object must be valid in the cluster; the Group and Kind must be registered in the cluster for this reference to be valid." + description: "ParentReference identifies an API object (usually a Gateway) that can be considered a parent of this resource (usually a route). There are two kinds of parent resources with \"Core\" support: \n * Gateway (Gateway conformance profile) * Service (Mesh conformance profile, experimental, ClusterIP Services only) \n This API may be extended in the future to support additional kinds of parent resources. \n The API object must be valid in the cluster; the Group and Kind must be registered in the cluster for this reference to be valid." type: object required: - name @@ -3021,7 +3429,7 @@ spec: maxLength: 253 pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ kind: - description: "Kind is kind of the referent. \n Support: Core (Gateway) \n Support: Implementation-specific (Other Resources)" + description: "Kind is kind of the referent. \n There are two kinds of parent resources with \"Core\" support: \n * Gateway (Gateway conformance profile) * Service (Mesh conformance profile, experimental, ClusterIP Services only) \n Support for other resources is Implementation-Specific." type: string default: Gateway maxLength: 63 @@ -3033,19 +3441,19 @@ spec: maxLength: 253 minLength: 1 namespace: - description: "Namespace is the namespace of the referent. When unspecified, this refers to the local namespace of the Route. \n Note that there are specific rules for ParentRefs which cross namespace boundaries. Cross-namespace references are only valid if they are explicitly allowed by something in the namespace they are referring to. For example: Gateway has the AllowedRoutes field, and ReferenceGrant provides a generic way to enable any other kind of cross-namespace reference. \n Support: Core" + description: "Namespace is the namespace of the referent. When unspecified, this refers to the local namespace of the Route. \n Note that there are specific rules for ParentRefs which cross namespace boundaries. Cross-namespace references are only valid if they are explicitly allowed by something in the namespace they are referring to. For example: Gateway has the AllowedRoutes field, and ReferenceGrant provides a generic way to enable any other kind of cross-namespace reference. \n ParentRefs from a Route to a Service in the same namespace are \"producer\" routes, which apply default routing rules to inbound connections from any namespace to the Service. \n ParentRefs from a Route to a Service in a different namespace are \"consumer\" routes, and these routing rules are only applied to outbound connections originating from the same namespace as the Route, for which the intended destination of the connections are a Service targeted as a ParentRef of the Route. \n Support: Core" type: string maxLength: 63 minLength: 1 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ port: - description: "Port is the network port this Route targets. It can be interpreted differently based on the type of parent resource. \n When the parent resource is a Gateway, this targets all listeners listening on the specified port that also support this kind of Route(and select this Route). It's not recommended to set `Port` unless the networking behaviors specified in a Route must apply to a specific port as opposed to a listener(s) whose port(s) may be changed. When both Port and SectionName are specified, the name and port of the selected listener must match both specified values. \n Implementations MAY choose to support other parent resources. Implementations supporting other types of parent resources MUST clearly document how/if Port is interpreted. \n For the purpose of status, an attachment is considered successful as long as the parent resource accepts it partially. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Extended \n " + description: "Port is the network port this Route targets. It can be interpreted differently based on the type of parent resource. \n When the parent resource is a Gateway, this targets all listeners listening on the specified port that also support this kind of Route(and select this Route). It's not recommended to set `Port` unless the networking behaviors specified in a Route must apply to a specific port as opposed to a listener(s) whose port(s) may be changed. When both Port and SectionName are specified, the name and port of the selected listener must match both specified values. \n When the parent resource is a Service, this targets a specific port in the Service spec. When both Port (experimental) and SectionName are specified, the name and port of the selected port must match both specified values. \n Implementations MAY choose to support other parent resources. Implementations supporting other types of parent resources MUST clearly document how/if Port is interpreted. \n For the purpose of status, an attachment is considered successful as long as the parent resource accepts it partially. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Extended \n " type: integer format: int32 maximum: 65535 minimum: 1 sectionName: - description: "SectionName is the name of a section within the target resource. In the following resources, SectionName is interpreted as the following: \n * Gateway: Listener Name. When both Port (experimental) and SectionName are specified, the name and port of the selected listener must match both specified values. \n Implementations MAY choose to support attaching Routes to other resources. If that is the case, they MUST clearly document how SectionName is interpreted. \n When unspecified (empty string), this will reference the entire resource. For the purpose of status, an attachment is considered successful if at least one section in the parent resource accepts it. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Core" + description: "SectionName is the name of a section within the target resource. In the following resources, SectionName is interpreted as the following: \n * Gateway: Listener Name. When both Port (experimental) and SectionName are specified, the name and port of the selected listener must match both specified values. * Service: Port Name. When both Port (experimental) and SectionName are specified, the name and port of the selected listener must match both specified values. Note that attaching Routes to Services as Parents is part of experimental Mesh support and is not supported for any other purpose. \n Implementations MAY choose to support attaching Routes to other resources. If that is the case, they MUST clearly document how SectionName is interpreted. \n When unspecified (empty string), this will reference the entire resource. For the purpose of status, an attachment is considered successful if at least one section in the parent resource accepts it. For example, Gateway listeners can restrict which Routes can attach to them by Route kind, namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from the referencing Route, the Route MUST be considered successfully attached. If no Gateway listeners accept attachment from this Route, the Route MUST be considered detached from the Gateway. \n Support: Core" type: string maxLength: 253 minLength: 1 @@ -3058,7 +3466,10 @@ spec: type: object properties: class: - description: The ingress class to use when creating Ingress resources to solve ACME challenges that use this challenge solver. Only one of 'class' or 'name' may be specified. + description: This field configures the annotation `kubernetes.io/ingress.class` when creating Ingress resources to solve ACME challenges that use this challenge solver. Only one of `class`, `name` or `ingressClassName` may be specified. + type: string + ingressClassName: + description: This field configures the field `ingressClassName` on the created Ingress resources used to solve ACME challenges that use this challenge solver. This is the recommended way of configuring the ingress class. Only one of `class`, `name` or `ingressClassName` may be specified. type: string ingressTemplate: description: Optional ingress template used to configure the ACME challenge solver ingress used for HTTP01 challenges. @@ -3079,7 +3490,7 @@ spec: additionalProperties: type: string name: - description: The name of the ingress resource that should have ACME challenge solving routes inserted into it in order to solve HTTP01 challenges. This is typically used in conjunction with ingress controllers like ingress-gce, which maintains a 1:1 mapping between external IPs and ingress resources. + description: The name of the ingress resource that should have ACME challenge solving routes inserted into it in order to solve HTTP01 challenges. This is typically used in conjunction with ingress controllers like ingress-gce, which maintains a 1:1 mapping between external IPs and ingress resources. Only one of `class`, `name` or `ingressClassName` may be specified. type: string podTemplate: description: Optional pod template used to configure the ACME challenge solver pods used for HTTP01 challenges. @@ -3100,7 +3511,7 @@ spec: additionalProperties: type: string spec: - description: PodSpec defines overrides for the HTTP01 challenge solver pod. Only the 'priorityClassName', 'nodeSelector', 'affinity', 'serviceAccountName' and 'tolerations' fields are supported currently. All other fields will be ignored. + description: PodSpec defines overrides for the HTTP01 challenge solver pod. Check ACMEChallengeSolverHTTP01IngressPodSpec to find out currently supported fields. All other fields will be ignored. type: object properties: affinity: @@ -3575,6 +3986,17 @@ spec: topologyKey: description: This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed. type: string + imagePullSecrets: + description: If specified, the pod's imagePullSecrets + type: array + items: + description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. + type: object + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + x-kubernetes-map-type: atomic nodeSelector: description: 'NodeSelector is a selector which must be true for the pod to fit on a node. Selector which must match a node''s labels for the pod to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/' type: object @@ -3642,372 +4064,145 @@ spec: type: array items: type: string - ocspServers: - description: The OCSP server list is an X.509 v3 extension that defines a list of URLs of OCSP responders. The OCSP responders can be queried for the revocation status of an issued certificate. If not set, the certificate will be issued with no OCSP servers set. For example, an OCSP server URL could be "http://ocsp.int-x3.letsencrypt.org". - type: array - items: - type: string - secretName: - description: SecretName is the name of the secret used to sign Certificates issued by this Issuer. - type: string - selfSigned: - description: SelfSigned configures this issuer to 'self sign' certificates using the private key used to create the CertificateRequest object. - type: object - properties: - crlDistributionPoints: - description: The CRL distribution points is an X.509 v3 certificate extension which identifies the location of the CRL from which the revocation of this certificate can be checked. If not set certificate will be issued without CDP. Values are strings. - type: array - items: - type: string - vault: - description: Vault configures this issuer to sign certificates using a HashiCorp Vault PKI backend. - type: object - required: - - auth - - path - - server - properties: - auth: - description: Auth configures how cert-manager authenticates with the Vault server. - type: object - properties: - appRole: - description: AppRole authenticates with Vault using the App Role auth mechanism, with the role and secret stored in a Kubernetes Secret resource. - type: object - required: - - path - - roleId - - secretRef - properties: - path: - description: 'Path where the App Role authentication backend is mounted in Vault, e.g: "approle"' - type: string - roleId: - description: RoleID configured in the App Role authentication backend when setting up the authentication backend in Vault. - type: string - secretRef: - description: Reference to a key in a Secret that contains the App Role secret used to authenticate with Vault. The `key` field must be specified and denotes which entry within the Secret resource is used as the app role secret. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - kubernetes: - description: Kubernetes authenticates with Vault by passing the ServiceAccount token stored in the named Secret resource to the Vault server. - type: object - required: - - role - - secretRef - properties: - mountPath: - description: The Vault mountPath here is the mount path to use when authenticating with Vault. For example, setting a value to `/v1/auth/foo`, will use the path `/v1/auth/foo/login` to authenticate with Vault. If unspecified, the default value "/v1/auth/kubernetes" will be used. - type: string - role: - description: A required field containing the Vault Role to assume. A Role binds a Kubernetes ServiceAccount with a set of Vault policies. - type: string - secretRef: - description: The required Secret field containing a Kubernetes ServiceAccount JWT used for authenticating with Vault. Use of 'ambient credentials' is not supported. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - tokenSecretRef: - description: TokenSecretRef authenticates with Vault by presenting a token. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - caBundle: - description: Base64-encoded bundle of PEM CAs which will be used to validate the certificate chain presented by Vault. Only used if using HTTPS to connect to Vault and ignored for HTTP connections. Mutually exclusive with CABundleSecretRef. If neither CABundle nor CABundleSecretRef are defined, the certificate bundle in the cert-manager controller container is used to validate the TLS connection. - type: string - format: byte - caBundleSecretRef: - description: Reference to a Secret containing a bundle of PEM-encoded CAs to use when verifying the certificate chain presented by Vault when using HTTPS. Mutually exclusive with CABundle. If neither CABundle nor CABundleSecretRef are defined, the certificate bundle in the cert-manager controller container is used to validate the TLS connection. If no key for the Secret is specified, cert-manager will default to 'ca.crt'. - type: object - required: - - name - properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - namespace: - description: 'Name of the vault namespace. Namespaces is a set of features within Vault Enterprise that allows Vault environments to support Secure Multi-tenancy. e.g: "ns1" More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces' - type: string - path: - description: 'Path is the mount path of the Vault PKI backend''s `sign` endpoint, e.g: "my_pki_mount/sign/my-role-name".' - type: string - server: - description: 'Server is the connection address for the Vault server, e.g: "https://vault.example.com:8200".' + ocspServers: + description: The OCSP server list is an X.509 v3 extension that defines a list of URLs of OCSP responders. The OCSP responders can be queried for the revocation status of an issued certificate. If not set, the certificate will be issued with no OCSP servers set. For example, an OCSP server URL could be "http://ocsp.int-x3.letsencrypt.org". + type: array + items: + type: string + secretName: + description: SecretName is the name of the secret used to sign Certificates issued by this Issuer. type: string - venafi: - description: Venafi configures this issuer to sign certificates using a Venafi TPP or Venafi Cloud policy zone. + selfSigned: + description: SelfSigned configures this issuer to 'self sign' certificates using the private key used to create the CertificateRequest object. + type: object + properties: + crlDistributionPoints: + description: The CRL distribution points is an X.509 v3 certificate extension which identifies the location of the CRL from which the revocation of this certificate can be checked. If not set certificate will be issued without CDP. Values are strings. + type: array + items: + type: string + vault: + description: Vault configures this issuer to sign certificates using a HashiCorp Vault PKI backend. type: object required: - - zone + - auth + - path + - server properties: - cloud: - description: Cloud specifies the Venafi cloud configuration settings. Only one of TPP or Cloud may be specified. + auth: + description: Auth configures how cert-manager authenticates with the Vault server. type: object - required: - - apiTokenSecretRef properties: - apiTokenSecretRef: - description: APITokenSecretRef is a secret key selector for the Venafi Cloud API token. + appRole: + description: AppRole authenticates with Vault using the App Role auth mechanism, with the role and secret stored in a Kubernetes Secret resource. type: object required: - - name + - path + - roleId + - secretRef properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + path: + description: 'Path where the App Role authentication backend is mounted in Vault, e.g: "approle"' type: string - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + roleId: + description: RoleID configured in the App Role authentication backend when setting up the authentication backend in Vault. type: string - url: - description: URL is the base URL for Venafi Cloud. Defaults to "https://api.venafi.cloud/v1". - type: string - tpp: - description: TPP specifies Trust Protection Platform configuration settings. Only one of TPP or Cloud may be specified. - type: object - required: - - credentialsRef - - url - properties: - caBundle: - description: Base64-encoded bundle of PEM CAs which will be used to validate the certificate chain presented by the TPP server. Only used if using HTTPS; ignored for HTTP. If undefined, the certificate bundle in the cert-manager controller container is used to validate the chain. - type: string - format: byte - credentialsRef: - description: CredentialsRef is a reference to a Secret containing the username and password for the TPP server. The secret must contain two keys, 'username' and 'password'. + secretRef: + description: Reference to a key in a Secret that contains the App Role secret used to authenticate with Vault. The `key` field must be specified and denotes which entry within the Secret resource is used as the app role secret. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + kubernetes: + description: Kubernetes authenticates with Vault by passing the ServiceAccount token stored in the named Secret resource to the Vault server. type: object required: - - name + - role properties: - name: - description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + mountPath: + description: The Vault mountPath here is the mount path to use when authenticating with Vault. For example, setting a value to `/v1/auth/foo`, will use the path `/v1/auth/foo/login` to authenticate with Vault. If unspecified, the default value "/v1/auth/kubernetes" will be used. type: string - url: - description: 'URL is the base URL for the vedsdk endpoint of the Venafi TPP instance, for example: "https://tpp.example.com/vedsdk".' - type: string - zone: - description: Zone is the Venafi Policy Zone to use for this issuer. All requests made to the Venafi platform will be restricted by the named zone policy. This field is required. - type: string - status: - description: Status of the Issuer. This is set and managed automatically. - type: object - properties: - acme: - description: ACME specific status options. This field should only be set if the Issuer is configured to use an ACME server to issue certificates. - type: object - properties: - lastRegisteredEmail: - description: LastRegisteredEmail is the email associated with the latest registered ACME account, in order to track changes made to registered account associated with the Issuer - type: string - uri: - description: URI is the unique account identifier, which can also be used to retrieve account details from the CA - type: string - conditions: - description: List of status conditions to indicate the status of a CertificateRequest. Known condition types are `Ready`. - type: array - items: - description: IssuerCondition contains condition information for an Issuer. - type: object - required: - - status - - type - properties: - lastTransitionTime: - description: LastTransitionTime is the timestamp corresponding to the last status change of this condition. - type: string - format: date-time - message: - description: Message is a human readable description of the details of the last transition, complementing reason. - type: string - observedGeneration: - description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Issuer. - type: integer - format: int64 - reason: - description: Reason is a brief machine readable explanation for the condition's last transition. - type: string - status: - description: Status of the condition, one of (`True`, `False`, `Unknown`). - type: string - enum: - - "True" - - "False" - - Unknown - type: - description: Type of the condition, known values are (`Ready`). - type: string - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - served: true - storage: true ---- -# Source: cert-manager/templates/crds.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: certificates.cert-manager.io - labels: - app: 'cert-manager' - app.kubernetes.io/name: 'cert-manager' - app.kubernetes.io/instance: 'cert-manager' - # Generated labels - app.kubernetes.io/version: "v1.11.0" -spec: - group: cert-manager.io - names: - kind: Certificate - listKind: CertificateList - plural: certificates - shortNames: - - cert - - certs - singular: certificate - categories: - - cert-manager - scope: Namespaced - versions: - - name: v1 - subresources: - status: {} - additionalPrinterColumns: - - jsonPath: .status.conditions[?(@.type=="Ready")].status - name: Ready - type: string - - jsonPath: .spec.secretName - name: Secret - type: string - - jsonPath: .spec.issuerRef.name - name: Issuer - priority: 1 - type: string - - jsonPath: .status.conditions[?(@.type=="Ready")].message - name: Status - priority: 1 - type: string - - jsonPath: .metadata.creationTimestamp - description: CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. - name: Age - type: date - schema: - openAPIV3Schema: - description: "A Certificate resource should be created to ensure an up to date and signed x509 certificate is stored in the Kubernetes Secret resource named in `spec.secretName`. \n The stored certificate will be renewed before it expires (as configured by `spec.renewBefore`)." - type: object - required: - - spec - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: Desired state of the Certificate resource. - type: object - required: - - issuerRef - - secretName - properties: - additionalOutputFormats: - description: AdditionalOutputFormats defines extra output formats of the private key and signed certificate chain to be written to this Certificate's target Secret. This is an Alpha Feature and is only enabled with the `--feature-gates=AdditionalCertificateOutputFormats=true` option on both the controller and webhook components. - type: array - items: - description: CertificateAdditionalOutputFormat defines an additional output format of a Certificate resource. These contain supplementary data formats of the signed certificate chain and paired private key. - type: object - required: - - type - properties: - type: - description: Type is the name of the format type that should be written to the Certificate's target Secret. - type: string - enum: - - DER - - CombinedPEM - commonName: - description: 'CommonName is a common name to be used on the Certificate. The CommonName should have a length of 64 characters or fewer to avoid generating invalid CSRs. This value is ignored by TLS clients when any subject alt name is set. This is x509 behaviour: https://tools.ietf.org/html/rfc6125#section-6.4.4' - type: string - dnsNames: - description: DNSNames is a list of DNS subjectAltNames to be set on the Certificate. - type: array - items: - type: string - duration: - description: The requested 'duration' (i.e. lifetime) of the Certificate. This option may be ignored/overridden by some issuer types. If unset this defaults to 90 days. Certificate will be renewed either 2/3 through its duration or `renewBefore` period before its expiry, whichever is later. Minimum accepted duration is 1 hour. Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration - type: string - emailAddresses: - description: EmailAddresses is a list of email subjectAltNames to be set on the Certificate. - type: array - items: - type: string - encodeUsagesInRequest: - description: EncodeUsagesInRequest controls whether key usages should be present in the CertificateRequest - type: boolean - ipAddresses: - description: IPAddresses is a list of IP address subjectAltNames to be set on the Certificate. - type: array - items: - type: string - isCA: - description: IsCA will mark this Certificate as valid for certificate signing. This will automatically add the `cert sign` usage to the list of `usages`. - type: boolean - issuerRef: - description: IssuerRef is a reference to the issuer for this certificate. If the `kind` field is not set, or set to `Issuer`, an Issuer resource with the given name in the same namespace as the Certificate will be used. If the `kind` field is set to `ClusterIssuer`, a ClusterIssuer with the provided name will be used. The `name` field in this stanza is required at all times. - type: object - required: - - name - properties: - group: - description: Group of the resource being referred to. + role: + description: A required field containing the Vault Role to assume. A Role binds a Kubernetes ServiceAccount with a set of Vault policies. + type: string + secretRef: + description: The required Secret field containing a Kubernetes ServiceAccount JWT used for authenticating with Vault. Use of 'ambient credentials' is not supported. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + serviceAccountRef: + description: A reference to a service account that will be used to request a bound token (also known as "projected token"). Compared to using "secretRef", using this field means that you don't rely on statically bound tokens. To use this field, you must configure an RBAC rule to let cert-manager request a token. + type: object + required: + - name + properties: + name: + description: Name of the ServiceAccount used to request a token. + type: string + tokenSecretRef: + description: TokenSecretRef authenticates with Vault by presenting a token. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + caBundle: + description: Base64-encoded bundle of PEM CAs which will be used to validate the certificate chain presented by Vault. Only used if using HTTPS to connect to Vault and ignored for HTTP connections. Mutually exclusive with CABundleSecretRef. If neither CABundle nor CABundleSecretRef are defined, the certificate bundle in the cert-manager controller container is used to validate the TLS connection. type: string - kind: - description: Kind of the resource being referred to. + format: byte + caBundleSecretRef: + description: Reference to a Secret containing a bundle of PEM-encoded CAs to use when verifying the certificate chain presented by Vault when using HTTPS. Mutually exclusive with CABundle. If neither CABundle nor CABundleSecretRef are defined, the certificate bundle in the cert-manager controller container is used to validate the TLS connection. If no key for the Secret is specified, cert-manager will default to 'ca.crt'. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Name of the vault namespace. Namespaces is a set of features within Vault Enterprise that allows Vault environments to support Secure Multi-tenancy. e.g: "ns1" More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces' type: string - name: - description: Name of the resource being referred to. + path: + description: 'Path is the mount path of the Vault PKI backend''s `sign` endpoint, e.g: "my_pki_mount/sign/my-role-name".' type: string - keystores: - description: Keystores configures additional keystore output formats stored in the `secretName` Secret resource. + server: + description: 'Server is the connection address for the Vault server, e.g: "https://vault.example.com:8200".' + type: string + venafi: + description: Venafi configures this issuer to sign certificates using a Venafi TPP or Venafi Cloud policy zone. type: object + required: + - zone properties: - jks: - description: JKS configures options for storing a JKS keystore in the `spec.secretName` Secret resource. + cloud: + description: Cloud specifies the Venafi cloud configuration settings. Only one of TPP or Cloud may be specified. type: object required: - - create - - passwordSecretRef + - apiTokenSecretRef properties: - create: - description: Create enables JKS keystore creation for the Certificate. If true, a file named `keystore.jks` will be created in the target Secret resource, encrypted using the password stored in `passwordSecretRef`. The keystore file will be updated immediately. A file named `truststore.jks` will also be created in the target Secret resource, encrypted using the password stored in `passwordSecretRef` containing the issuing Certificate Authority - type: boolean - passwordSecretRef: - description: PasswordSecretRef is a reference to a key in a Secret resource containing the password used to encrypt the JKS keystore. + apiTokenSecretRef: + description: APITokenSecretRef is a secret key selector for the Venafi Cloud API token. type: object required: - name @@ -4018,167 +4213,57 @@ spec: name: description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' type: string - pkcs12: - description: PKCS12 configures options for storing a PKCS12 keystore in the `spec.secretName` Secret resource. + url: + description: URL is the base URL for Venafi Cloud. Defaults to "https://api.venafi.cloud/v1". + type: string + tpp: + description: TPP specifies Trust Protection Platform configuration settings. Only one of TPP or Cloud may be specified. type: object required: - - create - - passwordSecretRef + - credentialsRef + - url properties: - create: - description: Create enables PKCS12 keystore creation for the Certificate. If true, a file named `keystore.p12` will be created in the target Secret resource, encrypted using the password stored in `passwordSecretRef`. The keystore file will be updated immediately. A file named `truststore.p12` will also be created in the target Secret resource, encrypted using the password stored in `passwordSecretRef` containing the issuing Certificate Authority - type: boolean - passwordSecretRef: - description: PasswordSecretRef is a reference to a key in a Secret resource containing the password used to encrypt the PKCS12 keystore. + caBundle: + description: Base64-encoded bundle of PEM CAs which will be used to validate the certificate chain presented by the TPP server. Only used if using HTTPS; ignored for HTTP. If undefined, the certificate bundle in the cert-manager controller container is used to validate the chain. + type: string + format: byte + credentialsRef: + description: CredentialsRef is a reference to a Secret containing the username and password for the TPP server. The secret must contain two keys, 'username' and 'password'. type: object required: - name properties: - key: - description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. - type: string name: description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' type: string - literalSubject: - description: LiteralSubject is an LDAP formatted string that represents the [X.509 Subject field](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6). Use this *instead* of the Subject field if you need to ensure the correct ordering of the RDN sequence, such as when issuing certs for LDAP authentication. See https://github.com/cert-manager/cert-manager/issues/3203, https://github.com/cert-manager/cert-manager/issues/4424. This field is alpha level and is only supported by cert-manager installations where LiteralCertificateSubject feature gate is enabled on both cert-manager controller and webhook. - type: string - privateKey: - description: Options to control private keys used for the Certificate. + url: + description: 'URL is the base URL for the vedsdk endpoint of the Venafi TPP instance, for example: "https://tpp.example.com/vedsdk".' + type: string + zone: + description: Zone is the Venafi Policy Zone to use for this issuer. All requests made to the Venafi platform will be restricted by the named zone policy. This field is required. + type: string + status: + description: Status of the Issuer. This is set and managed automatically. + type: object + properties: + acme: + description: ACME specific status options. This field should only be set if the Issuer is configured to use an ACME server to issue certificates. type: object properties: - algorithm: - description: Algorithm is the private key algorithm of the corresponding private key for this certificate. If provided, allowed values are either `RSA`,`Ed25519` or `ECDSA` If `algorithm` is specified and `size` is not provided, key size of 256 will be used for `ECDSA` key algorithm and key size of 2048 will be used for `RSA` key algorithm. key size is ignored when using the `Ed25519` key algorithm. - type: string - enum: - - RSA - - ECDSA - - Ed25519 - encoding: - description: The private key cryptography standards (PKCS) encoding for this certificate's private key to be encoded in. If provided, allowed values are `PKCS1` and `PKCS8` standing for PKCS#1 and PKCS#8, respectively. Defaults to `PKCS1` if not specified. + lastPrivateKeyHash: + description: LastPrivateKeyHash is a hash of the private key associated with the latest registered ACME account, in order to track changes made to registered account associated with the Issuer type: string - enum: - - PKCS1 - - PKCS8 - rotationPolicy: - description: RotationPolicy controls how private keys should be regenerated when a re-issuance is being processed. If set to Never, a private key will only be generated if one does not already exist in the target `spec.secretName`. If one does exists but it does not have the correct algorithm or size, a warning will be raised to await user intervention. If set to Always, a private key matching the specified requirements will be generated whenever a re-issuance occurs. Default is 'Never' for backward compatibility. + lastRegisteredEmail: + description: LastRegisteredEmail is the email associated with the latest registered ACME account, in order to track changes made to registered account associated with the Issuer type: string - enum: - - Never - - Always - size: - description: Size is the key bit size of the corresponding private key for this certificate. If `algorithm` is set to `RSA`, valid values are `2048`, `4096` or `8192`, and will default to `2048` if not specified. If `algorithm` is set to `ECDSA`, valid values are `256`, `384` or `521`, and will default to `256` if not specified. If `algorithm` is set to `Ed25519`, Size is ignored. No other values are allowed. - type: integer - renewBefore: - description: How long before the currently issued certificate's expiry cert-manager should renew the certificate. The default is 2/3 of the issued certificate's duration. Minimum accepted value is 5 minutes. Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration - type: string - revisionHistoryLimit: - description: revisionHistoryLimit is the maximum number of CertificateRequest revisions that are maintained in the Certificate's history. Each revision represents a single `CertificateRequest` created by this Certificate, either when it was created, renewed, or Spec was changed. Revisions will be removed by oldest first if the number of revisions exceeds this number. If set, revisionHistoryLimit must be a value of `1` or greater. If unset (`nil`), revisions will not be garbage collected. Default value is `nil`. - type: integer - format: int32 - secretName: - description: SecretName is the name of the secret resource that will be automatically created and managed by this Certificate resource. It will be populated with a private key and certificate, signed by the denoted issuer. - type: string - secretTemplate: - description: SecretTemplate defines annotations and labels to be copied to the Certificate's Secret. Labels and annotations on the Secret will be changed as they appear on the SecretTemplate when added or removed. SecretTemplate annotations are added in conjunction with, and cannot overwrite, the base set of annotations cert-manager sets on the Certificate's Secret. - type: object - properties: - annotations: - description: Annotations is a key value map to be copied to the target Kubernetes Secret. - type: object - additionalProperties: - type: string - labels: - description: Labels is a key value map to be copied to the target Kubernetes Secret. - type: object - additionalProperties: - type: string - subject: - description: Full X509 name specification (https://golang.org/pkg/crypto/x509/pkix/#Name). - type: object - properties: - countries: - description: Countries to be used on the Certificate. - type: array - items: - type: string - localities: - description: Cities to be used on the Certificate. - type: array - items: - type: string - organizationalUnits: - description: Organizational Units to be used on the Certificate. - type: array - items: - type: string - organizations: - description: Organizations to be used on the Certificate. - type: array - items: - type: string - postalCodes: - description: Postal codes to be used on the Certificate. - type: array - items: - type: string - provinces: - description: State/Provinces to be used on the Certificate. - type: array - items: - type: string - serialNumber: - description: Serial number to be used on the Certificate. + uri: + description: URI is the unique account identifier, which can also be used to retrieve account details from the CA type: string - streetAddresses: - description: Street addresses to be used on the Certificate. - type: array - items: - type: string - uris: - description: URIs is a list of URI subjectAltNames to be set on the Certificate. - type: array - items: - type: string - usages: - description: Usages is the set of x509 usages that are requested for the certificate. Defaults to `digital signature` and `key encipherment` if not specified. - type: array - items: - description: "KeyUsage specifies valid usage contexts for keys. See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3 https://tools.ietf.org/html/rfc5280#section-4.2.1.12 \n Valid KeyUsage values are as follows: \"signing\", \"digital signature\", \"content commitment\", \"key encipherment\", \"key agreement\", \"data encipherment\", \"cert sign\", \"crl sign\", \"encipher only\", \"decipher only\", \"any\", \"server auth\", \"client auth\", \"code signing\", \"email protection\", \"s/mime\", \"ipsec end system\", \"ipsec tunnel\", \"ipsec user\", \"timestamping\", \"ocsp signing\", \"microsoft sgc\", \"netscape sgc\"" - type: string - enum: - - signing - - digital signature - - content commitment - - key encipherment - - key agreement - - data encipherment - - cert sign - - crl sign - - encipher only - - decipher only - - any - - server auth - - client auth - - code signing - - email protection - - s/mime - - ipsec end system - - ipsec tunnel - - ipsec user - - timestamping - - ocsp signing - - microsoft sgc - - netscape sgc - status: - description: Status of the Certificate. This is set and managed automatically. - type: object - properties: conditions: - description: List of status conditions to indicate the status of certificates. Known condition types are `Ready` and `Issuing`. + description: List of status conditions to indicate the status of a CertificateRequest. Known condition types are `Ready`. type: array items: - description: CertificateCondition contains condition information for an Certificate. + description: IssuerCondition contains condition information for an Issuer. type: object required: - status @@ -4192,7 +4277,7 @@ spec: description: Message is a human readable description of the details of the last transition, complementing reason. type: string observedGeneration: - description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Certificate. + description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Issuer. type: integer format: int64 reason: @@ -4206,36 +4291,11 @@ spec: - "False" - Unknown type: - description: Type of the condition, known values are (`Ready`, `Issuing`). + description: Type of the condition, known values are (`Ready`). type: string x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map - failedIssuanceAttempts: - description: The number of continuous failed issuance attempts up till now. This field gets removed (if set) on a successful issuance and gets set to 1 if unset and an issuance has failed. If an issuance has failed, the delay till the next issuance will be calculated using formula time.Hour * 2 ^ (failedIssuanceAttempts - 1). - type: integer - lastFailureTime: - description: LastFailureTime is the time as recorded by the Certificate controller of the most recent failure to complete a CertificateRequest for this Certificate resource. If set, cert-manager will not re-request another Certificate until 1 hour has elapsed from this time. - type: string - format: date-time - nextPrivateKeySecretName: - description: The name of the Secret resource containing the private key to be used for the next certificate iteration. The keymanager controller will automatically set this field if the `Issuing` condition is set to `True`. It will automatically unset this field when the Issuing condition is not set or False. - type: string - notAfter: - description: The expiration time of the certificate stored in the secret named by this resource in `spec.secretName`. - type: string - format: date-time - notBefore: - description: The time after which the certificate stored in the secret named by this resource in spec.secretName is valid. - type: string - format: date-time - renewalTime: - description: RenewalTime is the time at which the certificate will be next renewed. If not set, no upcoming renewal is scheduled. - type: string - format: date-time - revision: - description: "The current 'revision' of the certificate as issued. \n When a CertificateRequest resource is created, it will have the `cert-manager.io/certificate-revision` set to one greater than the current value of this field. \n Upon issuance, this field will be set to the value of the annotation on the CertificateRequest resource used to issue the certificate. \n Persisting the value on the CertificateRequest resource allows the certificates controller to know whether a request is part of an old issuance or if it is part of the ongoing revision's issuance by checking if the revision value in the annotation is greater than this field." - type: integer served: true storage: true --- @@ -4249,7 +4309,7 @@ metadata: app.kubernetes.io/name: 'cert-manager' app.kubernetes.io/instance: 'cert-manager' # Generated labels - app.kubernetes.io/version: "v1.11.0" + app.kubernetes.io/version: "v1.13.1" spec: group: acme.cert-manager.io names: diff --git a/packages/helm/symphony/files/metric-middleware.json b/packages/helm/symphony/files/metric-middleware.json new file mode 100644 index 000000000..b814f7595 --- /dev/null +++ b/packages/helm/symphony/files/metric-middleware.json @@ -0,0 +1,29 @@ +{ + "type": "middleware.http.metrics", + "properties": { + "serviceName": "symphony-api", + "pipelines": [ + {{- if .Values.otelCollectorAddress }} + { + "exporter": { + "type": "metrics.exporters.otlpgrpc", + "collectorUrl": "{{ tpl .Values.otelCollectorAddress $ }}", + "temporality": false + } + } + {{- end }} + {{- if .Values.genevaCollectorAddress }} + {{- if .Values.otelCollectorAddress }} + , + {{- end }} + { + "exporter": { + "type": "metrics.exporters.otlpgrpc", + "collectorUrl": "{{ tpl .Values.genevaCollectorAddress $ }}", + "temporality": true + } + } + {{- end }} + ] + } + } \ No newline at end of file diff --git a/packages/helm/symphony/files/oss/delete-objects.sh b/packages/helm/symphony/files/oss/delete-objects.sh index 775a9662f..524286c8d 100755 --- a/packages/helm/symphony/files/oss/delete-objects.sh +++ b/packages/helm/symphony/files/oss/delete-objects.sh @@ -5,6 +5,7 @@ SOLUTION_GROUP=solution.symphony FABRIC_GROUP=fabric.symphony AI_GROUP=ai.symphony WORKFLOW_GROUP=workflow.symphony +FEDERATION_GROUP=federation.symphony function delete_crds { local resource_type=$1 @@ -24,3 +25,5 @@ delete_crds "devices.$FABRIC_GROUP" delete_crds "models.$AI_GROUP" delete_crds "skills.$AI_GROUP" delete_crds "skillpackages.$AI_GROUP" +delete_crds "catalogs.$FEDERATION_GROUP" +delete_crds "sites.$FEDERATION_GROUP" diff --git a/packages/helm/symphony/files/oss/remove-finalizers.sh b/packages/helm/symphony/files/oss/remove-finalizers.sh index 85a5ce923..c9d0d24b1 100755 --- a/packages/helm/symphony/files/oss/remove-finalizers.sh +++ b/packages/helm/symphony/files/oss/remove-finalizers.sh @@ -8,6 +8,7 @@ SOLUTION_GROUP=solution.symphony FABRIC_GROUP=fabric.symphony AI_GROUP=ai.symphony WORKFLOW_GROUP=workflow.symphony +FEDERATION_GROUP=federation.symphony patchResource() { local resource_type="$1" @@ -59,4 +60,6 @@ remove_finalizers "targets.$FABRIC_GROUP" remove_finalizers "devices.$FABRIC_GROUP" remove_finalizers "models.$AI_GROUP" remove_finalizers "skills.$AI_GROUP" -remove_finalizers "skillpackages.$AI_GROUP" \ No newline at end of file +remove_finalizers "skillpackages.$AI_GROUP" +remove_finalizers "catalogs.$FEDERATION_GROUP" +remove_finalizers "sites.$FEDERATION_GROUP" \ No newline at end of file diff --git a/packages/helm/symphony/files/symphony-api.json b/packages/helm/symphony/files/symphony-api.json index befba9948..a3647614f 100644 --- a/packages/helm/symphony/files/symphony-api.json +++ b/packages/helm/symphony/files/symphony-api.json @@ -212,7 +212,7 @@ "type": "managers.symphony.jobs", "properties": { "providers.state": "mem-state", - "baseUrl": "http://symphony-service:8080/v1alpha2/", + "baseUrl": {{ include "symphony.url" . | quote}}, "user": "admin", "password": "", "interval": "#15", @@ -584,7 +584,7 @@ { "type": "bindings.http", "config": { - "port": 8080, + "port": {{ include "symphony.apiContainerPortHttp" . }}, "pipeline": [ {{- include "symphony.zipkinMiddleware" . | indent 10 }} { @@ -673,12 +673,52 @@ { "type": "bindings.http", "config": { - "port": 8081, + "port": {{ include "symphony.apiContainerPortHttps" . }}, "tls": true, "certProvider": { - "type": "certs.autogen", - "config":{} - } + "type": "certs.localfile", + "config": { + "name": "symphony-serving-cert", + "cert": "{{- include "symphony.apiServingCert" . -}}", + "key": "{{- include "symphony.apiServingKey" . -}}" + } + }, + "pipeline": [ + {{- include "symphony.zipkinMiddleware" . | indent 10 }} + { + "type": "middleware.http.cors", + "properties": { + "Access-Control-Allow-Headers": "authorization,Content-Type", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Origin": "*" + } + }, + { + "type": "middleware.http.jwt", + "properties": { + "ignorePaths": [ + "/v1alpha2/users/auth", + "/v1alpha2/solution/instances", + "/v1alpha2/agent/references", + "/v1alpha2/greetings", + "/v1alpha2/agent/config" + ], + "verifyKey": "SymphonyKey", + "authServer": "kubernetes", + "enableRBAC": false + } + }, + { + "type": "middleware.http.telemetry", + "properties": { + "enabled": true, + "maxBatchSize": 8192, + "maxBatchIntervalSeconds": 2, + "client": "my-dev-machine" + } + } + ] } } ] diff --git a/packages/helm/symphony/files/trace-middleware.json b/packages/helm/symphony/files/trace-middleware.json new file mode 100644 index 000000000..a72d9ccd6 --- /dev/null +++ b/packages/helm/symphony/files/trace-middleware.json @@ -0,0 +1,16 @@ +{ + "type": "middleware.http.tracing", + "properties": { + "serviceName": "symphony-api", + "pipelines": [ + {{- if .Values.otelCollectorAddress }} + { + "exporter": { + "type": "tracing.exporters.otlpgrpc", + "collectorUrl": "{{ tpl .Values.otelCollectorAddress $ }}" + } + } + {{- end }} + ] + } + } \ No newline at end of file diff --git a/packages/helm/symphony/templates/certificate/certificate.yaml b/packages/helm/symphony/templates/certificate/certificate.yaml new file mode 100644 index 000000000..32a245d92 --- /dev/null +++ b/packages/helm/symphony/templates/certificate/certificate.yaml @@ -0,0 +1,15 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "symphony.apiServingCertName" . }} + namespace: {{ .Release.Namespace }} +spec: + issuerRef: + name: {{ include "symphony.apiServingCertIssuerName" . }} + kind: Issuer + secretName: {{ include "symphony.apiServingCertName" . }} + dnsNames: + - {{ include "symphony.serviceName" . }} + - {{ printf "%s.%s" (include "symphony.serviceName" .) .Release.Namespace }} + - {{ printf "%s.%s.svc" (include "symphony.serviceName" .) .Release.Namespace }} + - {{ printf "%s.%s.svc.cluster.local" (include "symphony.serviceName" .) .Release.Namespace }} \ No newline at end of file diff --git a/packages/helm/symphony/templates/env-configmap.yaml b/packages/helm/symphony/templates/env-configmap.yaml new file mode 100644 index 000000000..8efd5df6c --- /dev/null +++ b/packages/helm/symphony/templates/env-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "symphony.envConfigName" . }} + namespace: {{ .Release.Namespace }} +data: + APP_VERSION: {{ .Chart.AppVersion }} + CHART_VERSION: {{ .Chart.Version }} + API_SERVING_CA: {{ include "symphony.apiServingCA" . }} + SYMPHONY_API_URL: {{ include "symphony.url" . }} + USE_SERVICE_ACCOUNT_TOKENS: "true" \ No newline at end of file diff --git a/packages/helm/symphony/templates/extension-identity.yaml b/packages/helm/symphony/templates/identity/extension-identity.yaml similarity index 89% rename from packages/helm/symphony/templates/extension-identity.yaml rename to packages/helm/symphony/templates/identity/extension-identity.yaml index a82f4cddb..9b0be6a21 100644 --- a/packages/helm/symphony/templates/extension-identity.yaml +++ b/packages/helm/symphony/templates/identity/extension-identity.yaml @@ -1,4 +1,4 @@ -{{ if .Values.global.azure.identity.enabled -}} +{{ if .Values.global.azure.identity.isEnabled -}} apiVersion: clusterconfig.azure.com/v1beta1 kind: AzureExtensionIdentity metadata: diff --git a/packages/helm/symphony/templates/hooks-cluster-role-binding.yaml b/packages/helm/symphony/templates/identity/hooks-cluster-role-binding.yaml similarity index 100% rename from packages/helm/symphony/templates/hooks-cluster-role-binding.yaml rename to packages/helm/symphony/templates/identity/hooks-cluster-role-binding.yaml diff --git a/packages/helm/symphony/templates/hooks-cluster-role.yaml b/packages/helm/symphony/templates/identity/hooks-cluster-role.yaml similarity index 100% rename from packages/helm/symphony/templates/hooks-cluster-role.yaml rename to packages/helm/symphony/templates/identity/hooks-cluster-role.yaml diff --git a/packages/helm/symphony/templates/hooks-service-account.yaml b/packages/helm/symphony/templates/identity/hooks-service-account.yaml similarity index 100% rename from packages/helm/symphony/templates/hooks-service-account.yaml rename to packages/helm/symphony/templates/identity/hooks-service-account.yaml diff --git a/packages/helm/symphony/templates/pai-cluster-role-binding.yaml b/packages/helm/symphony/templates/identity/pai-cluster-role-binding.yaml similarity index 100% rename from packages/helm/symphony/templates/pai-cluster-role-binding.yaml rename to packages/helm/symphony/templates/identity/pai-cluster-role-binding.yaml diff --git a/packages/helm/symphony/templates/pai-cluster-role.yaml b/packages/helm/symphony/templates/identity/pai-cluster-role.yaml similarity index 100% rename from packages/helm/symphony/templates/pai-cluster-role.yaml rename to packages/helm/symphony/templates/identity/pai-cluster-role.yaml diff --git a/packages/helm/symphony/templates/pai-service-account.yaml b/packages/helm/symphony/templates/identity/pai-service-account.yaml similarity index 100% rename from packages/helm/symphony/templates/pai-service-account.yaml rename to packages/helm/symphony/templates/identity/pai-service-account.yaml diff --git a/packages/helm/symphony/templates/outbound-proxy-secret.yaml b/packages/helm/symphony/templates/outbound-proxy-secret.yaml new file mode 100644 index 000000000..223e757d6 --- /dev/null +++ b/packages/helm/symphony/templates/outbound-proxy-secret.yaml @@ -0,0 +1,29 @@ +{{ if .Values.Azure.proxySettings.isProxyEnabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "symphony.fullname" . }}-proxy-config + namespace: {{ .Release.Namespace }} +type: Opaque +data: +{{ if .Values.Azure.proxySettings.httpProxy }} + HTTP_PROXY: {{ .Values.Azure.proxySettings.httpProxy | b64enc | quote }} +{{end}} +{{ if .Values.Azure.proxySettings.httpsProxy }} + HTTPS_PROXY: {{ .Values.Azure.proxySettings.httpsProxy | b64enc | quote }} +{{end}} +{{ if .Values.Azure.proxySettings.noProxy }} + NO_PROXY: {{ .Values.Azure.proxySettings.noProxy| b64enc | quote }} +{{ end }} +{{ end }} +--- +{{ if and (or .Values.Azure.proxySettings.isCustomCert .Values.Azure.proxySettings.isProxyEnabled) (.Values.Azure.proxySettings.proxyCert) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "symphony.fullname" . }}-proxy-cert + namespace: {{ .Release.Namespace }} +type: Opaque +data: + proxy-cert.crt: {{ .Values.Azure.proxySettings.proxyCert | b64enc | quote }} +{{ end }} \ No newline at end of file diff --git a/packages/helm/symphony/templates/symphony-api.yaml b/packages/helm/symphony/templates/symphony-api.yaml deleted file mode 100644 index b288691fe..000000000 --- a/packages/helm/symphony/templates/symphony-api.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "symphony.fullname" . }}-api - labels: - app: {{ include "symphony.appSelector" . }} - namespace: {{ .Release.Namespace }} -spec: - replicas: 1 - selector: - matchLabels: - app: {{ include "symphony.appSelector" . }} - template: - metadata: - labels: - app: {{ include "symphony.appSelector" . }} - spec: - {{- with .Values.affinity }} - affinity: -{{ toYaml . | indent 8 }} - {{- end }} - serviceAccountName: {{ include "symphony.serviceAccountName" . }} - containers: - - name: symphony-api - securityContext: {{- toYaml .Values.securityContext | nindent 12 }} - image: {{ .Values.paiImage.repository }}:{{ .Values.paiImage.tag }} - imagePullPolicy: {{ .Values.paiImage.pullPolicy }} - ports: - - containerPort: 8080 - - containerPort: 8081 - env: - - name: "HELM_NAMESPACE" - value: default - - name: "CONFIG" - value: /etc/symphony-api/config/symphony-api.json - envFrom: - - secretRef: - name: {{ include "symphony.fullname" . }}-auth - volumeMounts: - - name: symphony-api-config - mountPath: /etc/symphony-api/config - {{- if .Values.global.azure.identity.mSIAdapterYaml }} - - name: msi-adapter - env: - - name: TOKEN_NAMESPACE - value: {{ .Release.Namespace }} - {{- .Values.global.azure.identity.mSIAdapterYaml | nindent 8 }} - {{- end }} - volumes: - - name: symphony-api-config - configMap: - name: {{ include "symphony.configmapName" . }} \ No newline at end of file diff --git a/packages/helm/symphony/templates/_helpers.tpl b/packages/helm/symphony/templates/symphony-core/_helpers.tpl similarity index 53% rename from packages/helm/symphony/templates/_helpers.tpl rename to packages/helm/symphony/templates/symphony-core/_helpers.tpl index 5f2cd8de8..45343e959 100644 --- a/packages/helm/symphony/templates/_helpers.tpl +++ b/packages/helm/symphony/templates/symphony-core/_helpers.tpl @@ -30,6 +30,13 @@ Create chart name and version as used by the chart label. {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} +{{/* +Symphony Service Name +*/}} +{{- define "symphony.serviceName" -}} +{{- printf "%s-service" (include "symphony.fullname" .) }} +{{- end }} + {{/* Common labels */}} @@ -64,6 +71,20 @@ Configmap Name {{- printf "%s-api-config" (include "symphony.fullname" .) }} {{- end }} +{{/* +Symphony Api Container Http Port +*/}} +{{- define "symphony.apiContainerPortHttp" -}} +{{- default 8080 .Values.api.apiContainerPortHttp }} +{{- end }} + +{{/* +Symphony Api Container Https Port +*/}} +{{- define "symphony.apiContainerPortHttps" -}} +{{- default 8081 .Values.api.apiContainerPortHttps }} +{{- end }} + {{/* App Selector */}} @@ -79,3 +100,58 @@ Zipkin Middleware {{ tpl (.Files.Get "files/zipkin-middleware.json") . }}, {{- end }} {{- end }} + +{{/* +Symphony API serving certs directory path +*/}} +{{- define "symphony.apiServingCertsDir" -}} +{{- printf "/etc/%s-api/tls" (include "symphony.fullname" .) }} +{{- end }} + +{{/* +Symphony API serving certificate path +*/}} +{{- define "symphony.apiServingCert" -}} +{{- printf "%s/%s" (include "symphony.apiServingCertsDir" .) "tls.crt" }} +{{- end }} + +{{/* +Symphony API serving certificate key path +*/}} +{{- define "symphony.apiServingKey" -}} +{{- printf "%s/%s" (include "symphony.apiServingCertsDir" .) "tls.key" }} +{{- end }} + +{{/* +Symphony API serving certificate Name +*/}} +{{- define "symphony.apiServingCertName" -}} +{{ printf "%s%s" (include "symphony.fullname" .) "-api-serving-cert"}} +{{- end }} + + +{{/* +Symphony API serving certificate CA path +*/}} +{{- define "symphony.apiServingCA" -}} +{{- printf "%s/%s" (include "symphony.apiServingCertsDir" .) "ca.crt" }} +{{- end }} + +{{/* +Symphony API ServingCertIssuerName +*/}} +{{- define "symphony.apiServingCertIssuerName" -}} +{{- printf "%s%s" (include "symphony.fullname" .) "-selfsigned-issuer"}} +{{- end }} + +{{/* +Symphony full url Endpoint +*/}} +{{- define "symphony.url" -}} +{{- printf "https://%s:%s/v1alpha2/" (include "symphony.serviceName" .) (include "symphony.apiContainerPortHttps" .) }} +{{- end }} + +{{/* Symphony Env Config Name */}} +{{- define "symphony.envConfigName" -}} +{{- printf "%s-env-config" (include "symphony.fullname" .) }} +{{- end }} \ No newline at end of file diff --git a/packages/helm/symphony/templates/delete-job.yaml b/packages/helm/symphony/templates/symphony-core/delete-job.yaml similarity index 100% rename from packages/helm/symphony/templates/delete-job.yaml rename to packages/helm/symphony/templates/symphony-core/delete-job.yaml diff --git a/packages/helm/symphony/templates/remove-finializers-job.yaml b/packages/helm/symphony/templates/symphony-core/remove-finializers-job.yaml similarity index 100% rename from packages/helm/symphony/templates/remove-finializers-job.yaml rename to packages/helm/symphony/templates/symphony-core/remove-finializers-job.yaml diff --git a/packages/helm/symphony/templates/symphony-core/symphony-api.yaml b/packages/helm/symphony/templates/symphony-core/symphony-api.yaml new file mode 100644 index 000000000..ddcb07f19 --- /dev/null +++ b/packages/helm/symphony/templates/symphony-core/symphony-api.yaml @@ -0,0 +1,110 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "symphony.fullname" . }}-api + labels: + app: {{ include "symphony.appSelector" . }} + namespace: {{ .Release.Namespace }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "symphony.appSelector" . }} + template: + metadata: + labels: + app: {{ include "symphony.appSelector" . }} + spec: + {{- with .Values.affinity }} + affinity: + {{ toYaml . | indent 8 }} + {{- end }} + serviceAccountName: {{ include "symphony.serviceAccountName" . }} + {{- if and .Values.Azure.proxySettings.isProxyEnabled .Values.Azure.proxySettings.proxyCert }} + initContainers: + - name: init-porxy-cert-dir + image: mcr.microsoft.com/mirror/docker/library/busybox:1.35 + command: ['sh', '-c', "mkdir -p /etc/pki/ca-trust/extracted/pem /etc/pki/ca-trust/extracted/openssl /etc/pki/ca-trust/extracted/java /etc/pki/ca-trust/extracted/edk2"] + volumeMounts: + - name: ssl-certs + mountPath: /etc/pki/ca-trust/extracted/ + {{ end }} + containers: + - name: symphony-api + securityContext: {{- toYaml .Values.securityContext | nindent 12 }} + image: {{ .Values.paiImage.repository }}:{{ .Values.paiImage.tag }} + imagePullPolicy: {{ .Values.paiImage.pullPolicy }} + ports: + - containerPort: {{ include "symphony.apiContainerPortHttp" . }} + - containerPort: {{ include "symphony.apiContainerPortHttps" . }} + env: + - name: "HELM_NAMESPACE" + value: default + - name: "CONFIG" + value: /etc/symphony-api/config/symphony-api.json + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_ACCOUNT_NAME + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + - name: SYMPHONY_CONTROLLER_SERVICE_ACCOUNT_NAME + value: '{{ include "symphony.fullname" . }}-controller-manager' + + envFrom: + - secretRef: + name: {{ include "symphony.fullname" . }}-auth + {{ if .Values.Azure.proxySettings.isProxyEnabled }} + - secretRef: + name: {{ include "symphony.fullname" . }}-proxy-config + {{ end }} + - configMapRef: + name: {{ include "symphony.envConfigName" . }} + + volumeMounts: + - name: symphony-api-config + mountPath: /etc/symphony-api/config + - mountPath: /var/run/secrets/tokens + name: symphony-api-token + - mountPath: {{ include "symphony.apiServingCertsDir" . }} + name: serving-cert + readOnly: true + {{- if and .Values.Azure.proxySettings.isProxyEnabled .Values.Azure.proxySettings.proxyCert }} + - name: ssl-certs + mountPath: /etc/pki/ca-trust/extracted/ + readOnly: false + - mountPath: /etc/pki/ca-trust/source/anchors/proxy-cert.crt + subPath: proxy-cert.crt + name: proxy-certstore + {{- end }} + + {{- if .Values.global.azure.identity.mSIAdapterYaml }} + - name: msi-adapter + env: + - name: TOKEN_NAMESPACE + value: {{ .Release.Namespace }} + {{- .Values.global.azure.identity.mSIAdapterYaml | nindent 8 }} + {{- end }} + volumes: + - name: symphony-api-config + configMap: + name: {{ include "symphony.configmapName" . }} + - name: symphony-api-token + projected: + sources: + - serviceAccountToken: + path: symphony-api-token + expirationSeconds: 600 + audience: {{ include "symphony.url" . }} + - name: serving-cert + secret: + secretName: {{ include "symphony.apiServingCertName" . }} + {{- if and .Values.Azure.proxySettings.isProxyEnabled .Values.Azure.proxySettings.proxyCert }} + - name: proxy-certstore + secret: + secretName: {{ include "symphony.fullname" . }}-proxy-cert + - name: ssl-certs + emptyDir: {} + {{ end }} diff --git a/packages/helm/symphony/templates/symphony-service-ext.yaml b/packages/helm/symphony/templates/symphony-core/symphony-service-ext.yaml similarity index 61% rename from packages/helm/symphony/templates/symphony-service-ext.yaml rename to packages/helm/symphony/templates/symphony-core/symphony-service-ext.yaml index 48d8292f6..c8108f294 100644 --- a/packages/helm/symphony/templates/symphony-service-ext.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphony-service-ext.yaml @@ -11,11 +11,11 @@ spec: type: LoadBalancer ports: - protocol: TCP - port: 8080 - targetPort: 8080 + port: {{ .Values.symphony.extension.httpport }} + targetPort: {{ include "symphony.apiContainerPortHttp" . }} name: http - protocol: TCP - port: 8081 - targetPort: 8081 + port: {{ .Values.symphony.extension.httpsport }} + targetPort: {{ include "symphony.apiContainerPortHttps" . }} name: https {{- end }} \ No newline at end of file diff --git a/packages/helm/symphony/templates/symphony-core/symphony-service.yaml b/packages/helm/symphony/templates/symphony-core/symphony-service.yaml new file mode 100644 index 000000000..9df86739e --- /dev/null +++ b/packages/helm/symphony/templates/symphony-core/symphony-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "symphony.serviceName" . }} + namespace: {{ .Release.Namespace }} +spec: + selector: + app: symphony-api + type: ClusterIP + ports: + - protocol: TCP + port: {{ .Values.symphony.incluster.httpport }} + targetPort: {{ include "symphony.apiContainerPortHttp" . }} + name: http + - protocol: TCP + port: {{ .Values.symphony.incluster.httpsport }} + targetPort: {{ include "symphony.apiContainerPortHttps" . }} + name: https \ No newline at end of file diff --git a/packages/helm/symphony/templates/symphony.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml similarity index 98% rename from packages/helm/symphony/templates/symphony.yaml rename to packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index 473f3fac4..ecd8a43a0 100644 --- a/packages/helm/symphony/templates/symphony.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml @@ -2068,6 +2068,15 @@ spec: value: '{{ .Chart.AppVersion }}' - name: CONFIG_NAME value: '{{ include "symphony.fullname" . }}-manager-config' + - name: SERVICE_ACCOUNT_NAME + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + - name: USE_SERVICE_ACCOUNT_TOKENS + value: "true" + envFrom: + - configMapRef: + name: '{{ include "symphony.envConfigName" . }}' image: '{{ .Values.symphonyImage.repository }}:{{ .Values.symphonyImage.tag }}' imagePullPolicy: '{{ .Values.symphonyImage.pullPolicy }}' @@ -2098,6 +2107,11 @@ spec: securityContext: allowPrivilegeEscalation: false volumeMounts: + - mountPath: /var/run/secrets/tokens + name: symphony-api-token + - mountPath: '{{ include "symphony.apiServingCertsDir" . }}' + name: api-ca-cert + readOnly: true - mountPath: /tmp/k8s-webhook-server/serving-certs name: cert readOnly: true @@ -2128,6 +2142,20 @@ spec: secret: defaultMode: 420 secretName: '{{ include "symphony.fullname" . }}-webhook-server-cert' + - name: symphony-api-token + projected: + sources: + - serviceAccountToken: + audience: '{{ include "symphony.url" . }}' + expirationSeconds: 600 + path: symphony-api-token + - name: api-ca-cert + secret: + defaultMode: 420 + items: + - key: ca.crt + path: ca.crt + secretName: '{{ include "symphony.apiServingCertName" . }}' --- apiVersion: cert-manager.io/v1 kind: Certificate diff --git a/packages/helm/symphony/templates/symphony-service.yaml b/packages/helm/symphony/templates/symphony-service.yaml deleted file mode 100644 index 7d67d936f..000000000 --- a/packages/helm/symphony/templates/symphony-service.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "symphony.fullname" . }}-service - namespace: {{ .Release.Namespace }} -spec: - selector: - app: symphony-api - type: ClusterIP - ports: - - protocol: TCP - port: 8080 - targetPort: 8080 - name: http - - protocol: TCP - port: 8081 - targetPort: 8081 - name: https \ No newline at end of file diff --git a/packages/helm/symphony/values.yaml b/packages/helm/symphony/values.yaml index 5fb3095d0..5de279268 100644 --- a/packages/helm/symphony/values.yaml +++ b/packages/helm/symphony/values.yaml @@ -12,7 +12,7 @@ installServiceExt: true global: azure: identity: - enabled: false + isEnabled: false observability: tracing: exporter: @@ -31,4 +31,21 @@ parent: username: admin password: siteId: hq -imagePrivateRegistryUrl: ghcr.io \ No newline at end of file +imagePrivateRegistryUrl: ghcr.io +api: + apiContainerPortHttp: 8080 + apiContainerPortHttps: 8081 +symphony: + incluster: + httpsport: 8081 + httpport: 8080 + extension: + httpsport: 8081 + httpport: 8080 +Azure: + proxySettings: + isProxyEnabled: false + httpProxy: "" + httpsProxy: "" + noProxy: "" + proxyCert: "" diff --git a/packages/testutils/.gitignore b/packages/testutils/.gitignore new file mode 100644 index 000000000..efa6632a6 --- /dev/null +++ b/packages/testutils/.gitignore @@ -0,0 +1 @@ +bin/* \ No newline at end of file diff --git a/packages/testutils/.vscode/settings.json b/packages/testutils/.vscode/settings.json new file mode 100644 index 000000000..c5dd2ba28 --- /dev/null +++ b/packages/testutils/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "go.buildTags": "mage" +} \ No newline at end of file diff --git a/packages/testutils/README.md b/packages/testutils/README.md new file mode 100644 index 000000000..ce173301b --- /dev/null +++ b/packages/testutils/README.md @@ -0,0 +1,136 @@ + + +# testutils + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils" +``` + +Package testutils provides utilities for testing. It provides 2 main interfaces: + +- Condition: a way to define conditions for an expectation +- Expectation: a way to define expectations for a resource + +This library also includes a set of predefined conditions and expectations for most common use cases. + +### Conditions + +The condition package comes with a few implementations of the Condition interface. Some of the most important constructors are: + +- CountCondition: Creates a condition that checks if the count of a resource is equal to a given value. Works for slices and maps. +- JqCondition: Creates a condition that checks if the result of a jq expression is valid for the resource\(s\). +- JsonPathCondition: Creates a condition that checks if the result of a jsonpath expression is valid for the resource\(s\). +- AllCondition: Creates a condition that checks if all of the given conditions are satisfied. +- AnyCondition: Creates a condition that checks if any of the given conditions is satisfied. + +These conditions can be used to create more complex conditions. For example, the expectations.kube package has a predefined condition helpers to check pod readiness defined like so: + +``` +var PodReadyCondition types.Condition = conditions.All( + conditions.NewKubernetesStatusCondition("Ready", true), + conditions.NewKubernetesStatusCondition("Initialized", true), + conditions.NewKubernetesStatusCondition("ContainersReady", true), +) +``` + +It uses the NewKubernetesStatusCondition constructor \(which itself uses the jsonpath condition constructor\) to create a conditions that checks if the status of a pod is equal to a given value. Then it combines all of these conditions using the AllCondition constructor. + +### Expectations + +The epectation package comes with 4 implementations of the Expectation interface: + +- KubernetesExpectation: an expectation for kubernetes resources +- HelmExpectation: an expectation for helm releases +- AllExpectation: an expectation for grouping multiple expectations and checking if all of them are satisfied +- AnyExpectation: an expectation for grouping multiple expectations and checking if any of them is satisfied + +### KubernetesExpectation + +The KubernetesExpectation is an expectation for kubernetes resources. It is satisfied if the resource is present or not present in the cluster \(depending on its configuration\). The main constructor of this expectation \`kube.Resource\` has 3 required parameters: + +- pattern: a regex pattern string that matches the name of the expected resource\(s\) +- namespace: the namespace of the expected resource\(s\). This is parameter is ignored if the resource is cluster\-scoped. If this resource should be matched in all namespaces, use the "\*" wildcard. +- gvk: the group, version, kind of the expected resource\(s\) + +The expectation also accepts a list of options that can be used to configure the expectation. See the options section of the package documentation for more details. + +Because some resources are commonly expected in the cluster, this package also provides a set of predefined expectations and constructors for them: + +- kube.AbsentResource: a constructor for an expectation for a resource that is not present in the cluster. ie: a resource that has been deleted or has a count of 0. +- kube.Pod: an expectation for a pod\(s\) in the cluster +- kube.AbsentPod: an expectation for a pod\(s\) that is not present in the cluster +- kube.Target: an expectation for a target\(s\) in the cluster +- kube.Solution: an expectation for a solution\(s\) in the cluster +- kube.Instance: an expectation for an instance\(s\) in the cluster + +### HelmExpectation + +The HelmExpectation is an expectation for helm releases. It is satisfied if the release is present or not present in the cluster \(depending on its configuration\). The main constructor of this expectation \`helm.New\` has 2 required parameters: + +- pattern: a regex pattern string that matches the name of the expected release\(s\) +- namespace: the namespace of the expected release\(s\). To match releases in all namespaces, use the "\*" wildcard. + +The expectation also accepts a list of options that can be used to configure the expectation. See the options section of the package documentation for more details. + +### AllExpectation + +The AllExpectation is an expectation for grouping multiple expectations and checking if all of them are satisfied. The main constructor of this expectation \`expectations.All\` accepts a list of expectations as parameters. + +### AnyExpectation + +The AnyExpectation is an expectation for grouping multiple expectations and checking if any of them is satisfied. The main constructor of this expectation \`expectations.Any\` accepts a list of expectations as parameters. + +### Examples + +Check if a pod named "my\-pod\-34jfk3\-fd4k56g" in namespace "default" exists in the cluster: + +``` +exp := kube.Must(kube.Pod("my-pod-34jfk3-fd4k56g", "default")) +if err := exp.Verify(ctx); err != nil { + // expectation failed. handle error +} +``` + +Check if there are 2 pods with prefix "my\-pod\-" in namespace "default" in the cluster and they are ready: + +``` +exp := kube.Must(kube.Pod( + "my-pod-.*", + "default", + kube.WithListCondition( + conditions.Count(2) + ), + kube.WithCondition(kube.PodReadyCondition), // PodReadyCondition is pre-defined in the kube package +)) +if err := exp.Verify(ctx); err != nil { + // expectation failed. handle error +} +``` + +Check if there are 2 pods with prefix "my\-pod\-" in namespace "default" in the cluster and that each pod is ready or initialized and each pod has a specific label + +``` +exp := kube.Must(kube.Pod( + "my-pod-.*", + "default", + kube.WithListCondition( + conditions.Count(2) + ), + kube.WithCondition(conditions.All( + conditions.Any( + conditions.NewKubernetesStatusCondition("Ready", true), + conditions.NewKubernetesStatusCondition("Initialized", true), + ), + kube.NewLabelMatchCondition("my-label", "my-value"), + )), +)) +if err := exp.Verify(ctx); err != nil { + // expectation failed. handle error +} +``` + +## Index + + + +Generated by [gomarkdoc]() diff --git a/packages/testutils/conditions/README.md b/packages/testutils/conditions/README.md new file mode 100644 index 000000000..46edd94f6 --- /dev/null +++ b/packages/testutils/conditions/README.md @@ -0,0 +1,137 @@ + + +# conditions + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/conditions" +``` + +## Index + +- [type AllCondition](<#AllCondition>) + - [func All\(conditions ...types.Condition\) \*AllCondition](<#All>) + - [func \(a \*AllCondition\) And\(conditions ...types.Condition\) types.Condition](<#AllCondition.And>) + - [func \(a \*AllCondition\) Description\(\) string](<#AllCondition.Description>) + - [func \(a \*AllCondition\) Id\(\) string](<#AllCondition.Id>) + - [func \(c \*AllCondition\) IsSatisfiedBy\(oc context.Context, resource interface\{\}\) error](<#AllCondition.IsSatisfiedBy>) + - [func \(a \*AllCondition\) WithCaching\(\) \*AllCondition](<#AllCondition.WithCaching>) +- [type AnyCondition](<#AnyCondition>) + - [func Any\(conditions ...types.Condition\) \*AnyCondition](<#Any>) + - [func \(a \*AnyCondition\) Description\(\) string](<#AnyCondition.Description>) + - [func \(a \*AnyCondition\) Id\(\) string](<#AnyCondition.Id>) + - [func \(c \*AnyCondition\) IsSatisfiedBy\(oc context.Context, resource interface\{\}\) error](<#AnyCondition.IsSatisfiedBy>) + + + +## type [AllCondition]() + + + +```go +type AllCondition struct { + // contains filtered or unexported fields +} +``` + + +### func [All]() + +```go +func All(conditions ...types.Condition) *AllCondition +``` + +All returns a new AllCondition. + + +### func \(\*AllCondition\) [And]() + +```go +func (a *AllCondition) And(conditions ...types.Condition) types.Condition +``` + +For internal use only. + + +### func \(\*AllCondition\) [Description]() + +```go +func (a *AllCondition) Description() string +``` + +Description implements types.Condition. + + +### func \(\*AllCondition\) [Id]() + +```go +func (a *AllCondition) Id() string +``` + +Id implements types.Condition. + + +### func \(\*AllCondition\) [IsSatisfiedBy]() + +```go +func (c *AllCondition) IsSatisfiedBy(oc context.Context, resource interface{}) error +``` + +IsSatisfiedBy implements types.Condition. + + +### func \(\*AllCondition\) [WithCaching]() + +```go +func (a *AllCondition) WithCaching() *AllCondition +``` + +WithCaching returns a new AllCondition with caching enabled. This means that if a condition is satisfied, it will not be checked again. + + +## type [AnyCondition]() + + + +```go +type AnyCondition struct { + // contains filtered or unexported fields +} +``` + + +### func [Any]() + +```go +func Any(conditions ...types.Condition) *AnyCondition +``` + +Any returns a condition that is satisfied if any of the given conditions are satisfied. + + +### func \(\*AnyCondition\) [Description]() + +```go +func (a *AnyCondition) Description() string +``` + +Description implements types.Condition. + + +### func \(\*AnyCondition\) [Id]() + +```go +func (a *AnyCondition) Id() string +``` + +Id implements types.Condition. + + +### func \(\*AnyCondition\) [IsSatisfiedBy]() + +```go +func (c *AnyCondition) IsSatisfiedBy(oc context.Context, resource interface{}) error +``` + +IsSatisfiedBy implements types.Condition. + +Generated by [gomarkdoc]() diff --git a/packages/testutils/conditions/all.go b/packages/testutils/conditions/all.go new file mode 100644 index 000000000..632a2996f --- /dev/null +++ b/packages/testutils/conditions/all.go @@ -0,0 +1,90 @@ +package conditions + +import ( + "context" + "fmt" + "strings" + + ectx "github.com/eclipse-symphony/symphony/packages/testutils/internal/context" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/google/uuid" +) + +type ( + AllCondition struct { + id string + conditions []types.Condition + successCache map[string]struct{} + shouldCache bool + level int + } +) + +var ( + _ types.Condition = &AllCondition{} +) + +// Description implements types.Condition. +func (a *AllCondition) Description() string { + return "all group" +} + +// Id implements types.Condition. +func (a *AllCondition) Id() string { + return a.id +} + +// IsSatisfiedBy implements types.Condition. +func (c *AllCondition) IsSatisfiedBy(oc context.Context, resource interface{}) error { + ctx := ectx.From(oc) + c.level = ctx.Level() + c.log("checking if all conditions are satisfied") + for i, condition := range c.conditions { + if _, ok := c.successCache[condition.Id()]; c.shouldCache && ok { + c.log("condition %d of %d was satisfied (cached) [%s]: skipping...", i+1, len(c.conditions), condition.Description()) + continue + } + c.log("checking condition %d of %d: [%s]", i+1, len(c.conditions), condition.Description()) + if err := condition.IsSatisfiedBy(ctx.Nested(), resource); err != nil { + c.log("condition %d of %d failed: %s", i+1, len(c.conditions), err) + return err + } + c.log("condition %d of %d was satisfied: [%s]", i+1, len(c.conditions), condition.Description()) + if c.shouldCache { + c.successCache[condition.Id()] = struct{}{} + } + } + c.log("all conditions were satisfied") + return nil +} + +func (c *AllCondition) log(str string, args ...interface{}) { + s := fmt.Sprintf(str, args...) + logger.GetDefaultLogger()("%s[%s]: %s\n", strings.Repeat(" ", c.level), c.Description(), s) +} + +// All returns a new AllCondition. +func All(conditions ...types.Condition) *AllCondition { + return &AllCondition{ + conditions: conditions, + successCache: make(map[string]struct{}), + id: uuid.NewString(), + } +} + +// WithCaching returns a new AllCondition with caching enabled. This means that +// if a condition is satisfied, it will not be checked again. +func (a *AllCondition) WithCaching() *AllCondition { + na := All(a.conditions...) + na.shouldCache = true + return na +} + +// For internal use only. +func (a *AllCondition) And(conditions ...types.Condition) types.Condition { + na := All(a.conditions...) + na.conditions = append(na.conditions, conditions...) + na.shouldCache = a.shouldCache + return na +} diff --git a/packages/testutils/conditions/any.go b/packages/testutils/conditions/any.go new file mode 100644 index 000000000..5675f6a39 --- /dev/null +++ b/packages/testutils/conditions/any.go @@ -0,0 +1,63 @@ +package conditions + +import ( + "context" + "fmt" + "strings" + + ectx "github.com/eclipse-symphony/symphony/packages/testutils/internal/context" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/google/uuid" +) + +type ( + AnyCondition struct { + id string + conditions []types.Condition + level int + } +) + +var ( + _ types.Condition = &AnyCondition{} +) + +// Description implements types.Condition. +func (a *AnyCondition) Description() string { + return "any group" +} + +// Id implements types.Condition. +func (a *AnyCondition) Id() string { + return a.id +} + +// IsSatisfiedBy implements types.Condition. +func (c *AnyCondition) IsSatisfiedBy(oc context.Context, resource interface{}) error { + ctx := ectx.From(oc) + c.level = ctx.Level() + c.log("checking if any condition is satisfied") + for i, condition := range c.conditions { + c.log("checking condition %d of %d: [%s]", i+1, len(c.conditions), condition.Description()) + if err := condition.IsSatisfiedBy(ctx.Nested(), resource); err == nil { + c.log("condition %d of %d was satisfied: [%s]", i+1, len(c.conditions), condition.Description()) + return nil + } + } + return fmt.Errorf("none of the conditions were satisfied") +} + +func (c *AnyCondition) log(str string, args ...interface{}) { + s := fmt.Sprintf(str, args...) + logger.GetDefaultLogger()("%s[%s]: %s\n", strings.Repeat(" ", c.level), c.Description(), s) +} + +// Any returns a condition that is satisfied if any of the given conditions are satisfied. +func Any(conditions ...types.Condition) *AnyCondition { + return &AnyCondition{ + conditions: conditions, + id: uuid.NewString(), + } +} diff --git a/packages/testutils/conditions/basic.go b/packages/testutils/conditions/basic.go new file mode 100644 index 000000000..e83d6f818 --- /dev/null +++ b/packages/testutils/conditions/basic.go @@ -0,0 +1,74 @@ +package conditions + +import ( + "context" + "fmt" + + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/google/uuid" +) + +type ( + basic struct { + id string + desc string + fn func(context.Context, interface{}) error + failMsg func(interface{}, error) string + } + basicopts func(*basic) +) + +var ( + _ types.Condition = basic{} +) + +// IsSatisfiedBy implements types.Condition. +func (b basic) IsSatisfiedBy(c context.Context, resource interface{}) error { + if err := b.fn(c, resource); err != nil { + return fmt.Errorf("%s", b.failMsg(resource, err)) + } + return nil +} + +// Id implements types.Condition. +func (ec basic) Id() string { + return ec.id +} + +// Description implements types.Condition. +func (ec basic) Description() string { + if ec.desc == "" { + return "basic condition" + } + return ec.desc +} + +// WithBasicDescription sets the description of the condition. +func WithBasicDescription(desc string) basicopts { + return func(b *basic) { + b.desc = desc + } +} + +// WithBasicFailureMessage sets the failure message of the condition. +func WithBasicFailureMessage(msg func(interface{}, error) string) basicopts { + return func(b *basic) { + b.failMsg = msg + } +} + +// Basic returns a basic condition. +func Basic(fn func(context.Context, interface{}) error, opts ...basicopts) basic { + b := basic{ + id: uuid.New().String(), + desc: "", + fn: fn, + failMsg: func(i interface{}, e error) string { return fmt.Sprintf("condition failed: %s", e.Error()) }, + } + + for _, opt := range opts { + opt(&b) + } + + return b +} diff --git a/packages/testutils/conditions/condition_test.go b/packages/testutils/conditions/condition_test.go new file mode 100644 index 000000000..6769f6194 --- /dev/null +++ b/packages/testutils/conditions/condition_test.go @@ -0,0 +1,310 @@ +package conditions + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/stretchr/testify/require" +) + +type ( + basicConditionTest struct { + name string + condition types.Condition + resource interface{} + wantErr bool + } + mmap = map[string]string +) + +func TestAll(t *testing.T) { + tests := []basicConditionTest{ + { + name: "should not return error if all conditions are satisfied - 1 right", + condition: All(Count(2)), + resource: []mmap{ + {"id": "1"}, + {"id": "2"}, + }, + }, + { + name: "should not return error if all conditions are satisfied - 2 right", + condition: All(Count(2), Count(2)), + resource: []mmap{ + {"id": "1"}, + {"id": "2"}, + }, + }, + { + name: "should return an error if any condition is not satisfied - 1 wrong", + condition: All(Count(2)), + resource: []mmap{ + {"id": "1"}, + }, + wantErr: true, + }, + { + name: "should return an error if any condition is not satisfied - 2 wrong", + condition: All(Count(2), Count(2)), + resource: []mmap{ + {"id": "1"}, + }, + wantErr: true, + }, + { + name: "should return an error if all conditions are not satisfied - 1 right, 1 wrong", + condition: All(Count(5), Count(1)), + resource: []mmap{ + {"id": "1"}, + }, + wantErr: true, + }, + { + name: "should return an error when any condition is not satisfied - nested", + condition: All( + All(Count(2)), + All( + Count(2), + Count(2), + ), + All( + Count(2), + All( + Count(1), // this one fails + ), + ), + ), + resource: []mmap{ + {"id": "1"}, + {"id": "1"}, + }, + wantErr: true, + }, + { + name: "should return not return error when all conditions are satisfied - nested", + condition: All( + All(Count(2)), + All( + Count(2), + Count(2), + ), + All( + Count(2), + All( + Count(2), + ), + ), + ), + resource: []mmap{ + {"id": "1"}, + {"id": "1"}, + }, + }, + } + testBasicConditions(t, tests) +} + +func TestBasicCondition(t *testing.T) { + cases := []basicConditionTest{ + { + name: "basic condition test", + condition: Basic(func(ctx context.Context, resource interface{}) error { + return nil + }), + }, + { + name: "basic condition test fail", + condition: Basic(func(ctx context.Context, resource interface{}) error { + return errors.New("fail") + }), + wantErr: true, + }, + } + testBasicConditions(t, cases) +} + +func TestBasicConditionWithOptions(t *testing.T) { + b := Basic( + func(ctx context.Context, resource interface{}) error { + r := resource.([]mmap) + if len(r) == 1 { + return nil + } + return errors.New("fail") + }, + WithBasicDescription("test description"), + WithBasicFailureMessage(func(resource interface{}, err error) string { + return "test failure message" + }), + ) + + require.Equal(t, "test description", b.Description()) + require.Equal(t, "test failure message", b.failMsg(nil, nil)) + +} +func TestAny(t *testing.T) { + tests := []basicConditionTest{ + { + name: "should not return error if any condition is satisfied - 1 right", + condition: Any(Count(1)), + resource: []mmap{ + {"id": "1"}, + }, + }, + { + name: "should not return error if any condition is satisfied - 2 right", + condition: Any(Count(1), Count(1)), + resource: []mmap{ + {"id": "1"}, + }, + }, + { + name: "should return an error if no condition is satisfied - 1 wrong", + condition: Any(Count(2)), + resource: []mmap{ + {"id": "1"}, + }, + wantErr: true, + }, + { + name: "should return an error if no condition is satisfied - 2 wrong", + condition: Any(Count(3), Count(2)), + resource: []mmap{ + {"id": "1"}, + }, + wantErr: true, + }, + { + name: "should not return an error if any condition is satisfied - 1 right, 1 wrong", + condition: Any(Count(1), Count(2)), + resource: []mmap{ + {"id": "1"}, + }, + }, + } + testBasicConditions(t, tests) +} + +func TestExpectedCount(t *testing.T) { + tests := []basicConditionTest{ + { + name: "should not return error if count is satisfied", + condition: Count(1), + resource: []mmap{ + {"id": "1"}, + }, + }, + { + name: "should return error if count is not satisfied", + condition: Count(2), + resource: []mmap{ + {"id": "1"}, + }, + wantErr: true, + }, + { + name: "should return error if expected type is not a slice, map or arry", + condition: Count(1), + resource: 1, + wantErr: true, + }, + } + + testBasicConditions(t, tests) +} +func TestCombo(t *testing.T) { + tests := []basicConditionTest{ + { + name: "combo - nested", + condition: Any( + Any(Count(2)), //false + Any( + Count(2), // false + Count(2), // false + ), // false + All( + Count(1), // true + Any( + Count(2), // false + Count(1), // true + ), // true + ), // true + ), // true + resource: []mmap{ + {"id": "1"}, + }, + }, + { + name: "should return error for combo", + condition: Any( + Any(Count(2)), //false + All( + Count(2), // false + Count(1), // true + GreaterThan(1), // false + ), // false + Any( + Count(2), // false + Any( + Count(2), // false + ), // false + All( + GreaterThan(1), // false + Count(1), // true + ), // false + ), // false + ), // false + resource: []mmap{ + {"id": "1"}, + }, + wantErr: true, + }, + } + testBasicConditions(t, tests) +} + +func TestExtendAllAndWithMoreConditions(t *testing.T) { + firstAll := All(Count(1)) + secondAll := firstAll.And(Count(1)) + + require.NotEqual(t, firstAll, secondAll) +} + +func TestAllWithCache(t *testing.T) { + callCount := 0 + c := All( + CountComparator(func(c int) bool { + callCount++ + return c == 1 + }, ""), // this will allways succeed + Count(0), // this will allways fail + ).WithCaching() + resource := []mmap{{"id": "1"}} + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + defer cancel() + + require.Equal(t, 0, callCount) + require.Error(t, c.IsSatisfiedBy(ctx, resource)) + require.Equal(t, 1, callCount) + + require.Error(t, c.IsSatisfiedBy(ctx, resource)) + require.Equal(t, 1, callCount) +} + +func testBasicConditions(t *testing.T, tt []basicConditionTest) { + for _, tt := range tt { + t.Run(tt.name, func(t *testing.T) { + require.NotEmpty(t, tt.condition.Id()) + require.NotEmpty(t, tt.condition.Description()) + err := tt.condition.IsSatisfiedBy(context.Background(), tt.resource) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/packages/testutils/conditions/count.go b/packages/testutils/conditions/count.go new file mode 100644 index 000000000..f519e5a42 --- /dev/null +++ b/packages/testutils/conditions/count.go @@ -0,0 +1,39 @@ +package conditions + +import ( + "context" + "fmt" + + "reflect" +) + +// CountComparator returns a condition that checks the count of a countable +// based on the given comparator function. +func CountComparator(fn func(int) bool, failMessage string) basic { + b := Basic(func(ctx context.Context, i interface{}) error { + switch reflect.TypeOf(i).Kind() { + case reflect.Slice, reflect.Array, reflect.Map, reflect.String: + if !fn(reflect.ValueOf(i).Len()) { + return fmt.Errorf(failMessage) + } + default: + return fmt.Errorf("expected countable, got %T", i) + } + return nil + }) + return b +} + +// Count returns a condition that checks the count of a countable. +func Count(count int) basic { + return CountComparator(func(c int) bool { + return c == count + }, fmt.Sprintf("expected count %d", count)) +} + +// GreaterThan returns a condition that checks the count of a countable. +func GreaterThan(count int) basic { + return CountComparator(func(c int) bool { + return c > count + }, fmt.Sprintf("expected count greater than %d", count)) +} diff --git a/packages/testutils/conditions/jq/README.md b/packages/testutils/conditions/jq/README.md new file mode 100644 index 000000000..c156dd8ab --- /dev/null +++ b/packages/testutils/conditions/jq/README.md @@ -0,0 +1,135 @@ + + +# jq + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/conditions/jq" +``` + +## Index + +- [type JqCondition](<#JqCondition>) + - [func Equality\(path string, value interface\{\}, opts ...Option\) \*JqCondition](<#Equality>) + - [func MustNew\(path string, opts ...Option\) \*JqCondition](<#MustNew>) + - [func New\(path string, opts ...Option\) \(\*JqCondition, error\)](<#New>) + - [func \(j \*JqCondition\) Description\(\) string](<#JqCondition.Description>) + - [func \(j \*JqCondition\) Id\(\) string](<#JqCondition.Id>) + - [func \(j \*JqCondition\) IsSatisfiedBy\(c context.Context, resource interface\{\}\) error](<#JqCondition.IsSatisfiedBy>) +- [type Option](<#Option>) + - [func WithCustomMatcher\(matcher func\(ctx context.Context, value, root interface\{\}, log logger.Logger\) error\) Option](<#WithCustomMatcher>) + - [func WithDescription\(description string\) Option](<#WithDescription>) + - [func WithLogger\(log func\(format string, args ...interface\{\}\)\) Option](<#WithLogger>) + - [func WithValue\(value interface\{\}\) Option](<#WithValue>) + + + +## type [JqCondition]() + + + +```go +type JqCondition struct { + // contains filtered or unexported fields +} +``` + + +### func [Equality]() + +```go +func Equality(path string, value interface{}, opts ...Option) *JqCondition +``` + +Equality returns a new JqCondition that checks if the value at the given path resolves to the given value exactly. + + +### func [MustNew]() + +```go +func MustNew(path string, opts ...Option) *JqCondition +``` + +MustNew returns a new JqCondition. It panics if the condition cannot be created. + + +### func [New]() + +```go +func New(path string, opts ...Option) (*JqCondition, error) +``` + +New returns a new JqCondition. + + +### func \(\*JqCondition\) [Description]() + +```go +func (j *JqCondition) Description() string +``` + +Description implements condition.Condition. + + +### func \(\*JqCondition\) [Id]() + +```go +func (j *JqCondition) Id() string +``` + +Id implements condition.Condition. + + +### func \(\*JqCondition\) [IsSatisfiedBy]() + +```go +func (j *JqCondition) IsSatisfiedBy(c context.Context, resource interface{}) error +``` + +IsSatisfiedBy implements condition.Condition. + + +## type [Option]() + + + +```go +type Option func(*JqCondition) +``` + + +### func [WithCustomMatcher]() + +```go +func WithCustomMatcher(matcher func(ctx context.Context, value, root interface{}, log logger.Logger) error) Option +``` + +WithCustomMatcher specifies the matcher to be used to match the jq result. + + +### func [WithDescription]() + +```go +func WithDescription(description string) Option +``` + +WithDescription specifies the description of the condition. + + +### func [WithLogger]() + +```go +func WithLogger(log func(format string, args ...interface{})) Option +``` + +WithLogger specifies the logger to be used to log the jq operations. + + +### func [WithValue]() + +```go +func WithValue(value interface{}) Option +``` + +WithValue does an equality check on the jq result. + +Generated by [gomarkdoc]() diff --git a/packages/testutils/conditions/jq/common.go b/packages/testutils/conditions/jq/common.go new file mode 100644 index 000000000..118793993 --- /dev/null +++ b/packages/testutils/conditions/jq/common.go @@ -0,0 +1,6 @@ +package jq + +// Equality returns a new JqCondition that checks if the value at the given path resolves to the given value exactly. +func Equality(path string, value interface{}, opts ...Option) *JqCondition { + return MustNew(path, append(opts, WithValue(value))...) +} diff --git a/packages/testutils/conditions/jq/jq.go b/packages/testutils/conditions/jq/jq.go new file mode 100644 index 000000000..c91670e09 --- /dev/null +++ b/packages/testutils/conditions/jq/jq.go @@ -0,0 +1,167 @@ +package jq + +import ( + "context" + "fmt" + "reflect" + "strings" + + ectx "github.com/eclipse-symphony/symphony/packages/testutils/internal/context" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/google/uuid" + "github.com/itchyny/gojq" +) + +type ( + JqCondition struct { + id string + matcher func(ctx context.Context, values, root interface{}, log logger.Logger) error + jq *gojq.Query + description string + l func(format string, args ...interface{}) + path string + level int + } + + Option func(*JqCondition) +) + +var ( + _ types.Condition = &JqCondition{} +) + +// WithCustomMatcher specifies the matcher to be used to match the jq result. +func WithCustomMatcher(matcher func(ctx context.Context, value, root interface{}, log logger.Logger) error) Option { + return func(j *JqCondition) { + j.matcher = matcher + } +} + +// WithLogger specifies the logger to be used to log the jq operations. +func WithLogger(log func(format string, args ...interface{})) Option { + return func(j *JqCondition) { + j.l = log + } +} + +// WithValue does an equality check on the jq result. +func WithValue(value interface{}) Option { + return func(j *JqCondition) { + t1 := reflect.TypeOf(value) + j.matcher = func(ctx context.Context, resolved, root interface{}, log logger.Logger) error { + j.log("Comparing %v with %v", value, resolved) + if resolved == nil { + if value == nil { + return nil + } + return fmt.Errorf("expected %v, got nil", value) + } + + if !reflect.TypeOf(resolved).ConvertibleTo(t1) { + return fmt.Errorf("expected %v, got %v", value, resolved) + } + + if reflect.DeepEqual(value, reflect.ValueOf(resolved).Convert(t1).Interface()) { + return nil + } + return fmt.Errorf("expected %v, got %v", value, resolved) + } + } +} + +// WithDescription specifies the description of the condition. +func WithDescription(description string) Option { + return func(j *JqCondition) { + j.description = description + } +} + +// New returns a new JqCondition. +func New(path string, opts ...Option) (*JqCondition, error) { + jqc := JqCondition{ + matcher: defaultMatcher, + path: path, + id: uuid.NewString(), + } + + jq, err := gojq.Parse(path) + if err != nil { + return nil, err + } + jqc.jq = jq + + for _, opt := range opts { + opt(&jqc) + } + + return &jqc, nil +} + +func must(jqc *JqCondition, err error) *JqCondition { + if err != nil { + panic(err) + } + return jqc +} + +// MustNew returns a new JqCondition. It panics if the condition cannot be created. +func MustNew(path string, opts ...Option) *JqCondition { + return must(New(path, opts...)) +} + +// IsSatisfiedBy implements condition.Condition. +func (j *JqCondition) IsSatisfiedBy(c context.Context, resource interface{}) error { + ctx := ectx.From(c) + j.level = ctx.Level() + j.log("Evaluating jq condition on resource") + + iter := j.jq.RunWithContext(ctx, resource) + for { + v, ok := iter.Next() + if !ok { + break + } + if err, ok := v.(error); ok { + return err + } + err := j.matcher(ctx, v, resource, j.log) + if err != nil { + return err + } + } + + return nil +} + +// Id implements condition.Condition. +func (j *JqCondition) Id() string { + return j.id +} + +// Description implements condition.Condition. +func (j *JqCondition) Description() string { + if j.description != "" { + return j.description + } + return j.path +} + +func (j *JqCondition) log(format string, args ...interface{}) { + s := fmt.Sprintf(format, args...) + format = "%s[%s]: %s\n" + args = []interface{}{strings.Repeat(" ", j.level), j.Description(), s} + + if j.l != nil { + j.l(format, args...) + } else { + logger.GetDefaultLogger()(format, args...) + } +} + +func defaultMatcher(ctx context.Context, value, root interface{}, log logger.Logger) error { + if value != nil { + return nil + } + return fmt.Errorf("expected non-empty result, got empty result") +} diff --git a/packages/testutils/conditions/jq/jq_test.go b/packages/testutils/conditions/jq/jq_test.go new file mode 100644 index 000000000..b0146ac7c --- /dev/null +++ b/packages/testutils/conditions/jq/jq_test.go @@ -0,0 +1,169 @@ +package jq + +import ( + "context" + "errors" + "testing" + + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/stretchr/testify/require" +) + +type ( + jqTestCase struct { + name string + j *JqCondition + resource any + wantErr bool + } + mmap = map[string]interface{} +) + +func TestJQCondition_IsSatisfiedBy(t *testing.T) { + var ( + foobar = mmap{ + "foo": mmap{ + "bar": "baz", + "nullValue": nil, + }, + } + basicCases = []jqTestCase{ + { + name: "should return no error if jq query matches value", + j: MustNew(`.foo.bar`, WithValue("baz")), + resource: foobar, + }, + { + name: "should return error if jq query dosesn't match value", + j: MustNew(`.foo.bar`, WithValue("wrong")), + resource: foobar, + wantErr: true, + }, + { + name: "should return no error if jq query matches for custom matcher", + j: MustNew(`.foo.bar == "baz"`, WithCustomMatcher(customTruthyTestMatcher)), + resource: foobar, + }, + { + name: "should return error if jq query does not match for custom matcher", + j: MustNew(`.foo.bar != "baz"`, WithCustomMatcher(customTruthyTestMatcher)), + resource: foobar, + wantErr: true, + }, + { + name: "should return error when path resolves to non-existent and no matcher is specified", + j: MustNew(".foo.bar.nonexistent"), + resource: foobar, + wantErr: true, + }, + { + name: "should return error when path resolves to nil value and no matcher is specified", + j: MustNew(".foo.nullValue"), + resource: foobar, + wantErr: true, + }, + { + name: "should handle nil resource", + j: Equality(".foo.nullValue", nil), + resource: foobar, + }, + { + name: "should handle nil resource and nil wrong value", + j: Equality(".foo.nullValue", "wrong"), + resource: foobar, + wantErr: true, + }, + { + name: "should handle incompativle types", + j: Equality(".foo.bar", 1), + resource: foobar, + wantErr: true, + }, + { + name: "should return no error when path resolves to non nil and no matcher is specified", + j: MustNew(".foo.bar"), + resource: foobar, + }, + { + name: "should accept a custom logger", + j: MustNew(".foo.bar", WithLogger(customTestLogger)), + resource: foobar, + }, + { + name: "should return error for unsupported type", + j: MustNew(".foo.bar", WithValue("baz")), + resource: struct{ name string }{name: "test"}, + wantErr: true, + }, + { + name: "should pass ussing helper function", + j: Equality(".foo.bar", "baz"), + resource: foobar, + }, + { + name: "should fail ussing helper function", + j: Equality(".foo.bar", "wrong"), + resource: foobar, + wantErr: true, + }, + } + ) + for _, tt := range basicCases { + t.Run(tt.name, func(t *testing.T) { + err := tt.j.IsSatisfiedBy(context.Background(), tt.resource) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestHasId(t *testing.T) { + jq := MustNew(`.foo.bar`) + require.NotEmpty(t, jq.Id()) +} + +func TestJQCondition_Description(t *testing.T) { + tests := []struct { + name string + description string + path string + want string + }{ + { + name: "should return description if set", + description: "test description", + path: `.foo == "bar"`, + want: "test description", + }, + { + name: "should return path if description not set", + path: `.foo == "bar"`, + want: `.foo == "bar"`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + j := must(New(tt.path, WithDescription(tt.description))) + + require.Equal(t, tt.want, j.Description()) + }) + } +} + +func TestShouldPanicForInvalidJQ(t *testing.T) { + require.Panics(t, func() { + MustNew(`some invalid jq query`) + }) +} + +func customTruthyTestMatcher(ctx context.Context, value, root interface{}, log logger.Logger) error { + if value.(bool) == true { + return nil + } + return errors.New("jq query did not match") +} + +func customTestLogger(format string, args ...interface{}) {} diff --git a/packages/testutils/conditions/jsonpath/README.md b/packages/testutils/conditions/jsonpath/README.md new file mode 100644 index 000000000..d993a5d23 --- /dev/null +++ b/packages/testutils/conditions/jsonpath/README.md @@ -0,0 +1,125 @@ + + +# jsonpath + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/conditions/jsonpath" +``` + +## Index + +- [type JpCondition](<#JpCondition>) + - [func MustNew\(path string, opts ...Option\) \*JpCondition](<#MustNew>) + - [func New\(path string, opts ...Option\) \(\*JpCondition, error\)](<#New>) + - [func \(j \*JpCondition\) Description\(\) string](<#JpCondition.Description>) + - [func \(j \*JpCondition\) Id\(\) string](<#JpCondition.Id>) + - [func \(j \*JpCondition\) IsSatisfiedBy\(c context.Context, resource interface\{\}\) error](<#JpCondition.IsSatisfiedBy>) +- [type Option](<#Option>) + - [func WithCustomMatcher\(matcher func\(ctx context.Context, value, root interface\{\}, log logger.Logger\) error\) Option](<#WithCustomMatcher>) + - [func WithDescription\(description string\) Option](<#WithDescription>) + - [func WithLogger\(log func\(format string, args ...interface\{\}\)\) Option](<#WithLogger>) + - [func WithValue\(value interface\{\}\) Option](<#WithValue>) + + + +## type [JpCondition]() + + + +```go +type JpCondition struct { + // contains filtered or unexported fields +} +``` + + +### func [MustNew]() + +```go +func MustNew(path string, opts ...Option) *JpCondition +``` + +MustNew returns a new JqCondition. It panics if the condition cannot be created. + + +### func [New]() + +```go +func New(path string, opts ...Option) (*JpCondition, error) +``` + +New returns a new JqCondition. + + +### func \(\*JpCondition\) [Description]() + +```go +func (j *JpCondition) Description() string +``` + +Description implements types.Condition. + + +### func \(\*JpCondition\) [Id]() + +```go +func (j *JpCondition) Id() string +``` + +Id implements types.Condition. + + +### func \(\*JpCondition\) [IsSatisfiedBy]() + +```go +func (j *JpCondition) IsSatisfiedBy(c context.Context, resource interface{}) error +``` + +IsSatisfiedBy implements condition.Condition. + + +## type [Option]() + + + +```go +type Option func(*JpCondition) +``` + + +### func [WithCustomMatcher]() + +```go +func WithCustomMatcher(matcher func(ctx context.Context, value, root interface{}, log logger.Logger) error) Option +``` + +WithCustomMatcher specifies the matcher to be used to match the jsonpath result. + + +### func [WithDescription]() + +```go +func WithDescription(description string) Option +``` + +WithDescription sets the description of the condition. + + +### func [WithLogger]() + +```go +func WithLogger(log func(format string, args ...interface{})) Option +``` + +WithLogger specifies the logger to be used to log the jsonpath operations. + + +### func [WithValue]() + +```go +func WithValue(value interface{}) Option +``` + +WithValue does an equality check on the jsonpath result. + +Generated by [gomarkdoc]() diff --git a/packages/testutils/conditions/jsonpath/jsonpath.go b/packages/testutils/conditions/jsonpath/jsonpath.go new file mode 100644 index 000000000..3012ae904 --- /dev/null +++ b/packages/testutils/conditions/jsonpath/jsonpath.go @@ -0,0 +1,143 @@ +package jsonpath + +import ( + "context" + "fmt" + "reflect" + "strings" + + ectx "github.com/eclipse-symphony/symphony/packages/testutils/internal/context" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/google/uuid" + "github.com/oliveagle/jsonpath" +) + +type ( + JpCondition struct { + id string + matcher func(ctx context.Context, values, root interface{}, log logger.Logger) error + jp *jsonpath.Compiled + description string + l func(format string, args ...interface{}) + level int + } + + Option func(*JpCondition) +) + +var ( + _ types.Condition = &JpCondition{} +) + +// WithCustomMatcher specifies the matcher to be used to match the jsonpath result. +func WithCustomMatcher(matcher func(ctx context.Context, value, root interface{}, log logger.Logger) error) Option { + return func(j *JpCondition) { + j.matcher = matcher + } +} + +// WithLogger specifies the logger to be used to log the jsonpath operations. +func WithLogger(log func(format string, args ...interface{})) Option { + return func(j *JpCondition) { + j.l = log + } +} + +// WithValue does an equality check on the jsonpath result. +func WithValue(value interface{}) Option { + return func(j *JpCondition) { + j.matcher = func(ctx context.Context, resolved, root interface{}, log logger.Logger) error { + j.log("Comparing %v with %s", value, resolved) + + if reflect.DeepEqual(value, resolved) { + return nil + } + return fmt.Errorf("expected %v, got %v", value, resolved) + } + } +} + +// WithDescription sets the description of the condition. +func WithDescription(description string) Option { + return func(j *JpCondition) { + j.description = description + } +} + +// New returns a new JqCondition. +func New(path string, opts ...Option) (*JpCondition, error) { + jpc := JpCondition{ + matcher: defaultMatcher, + id: uuid.NewString(), + } + + jp, err := jsonpath.Compile(path) + if err != nil { + return nil, err + } + jpc.jp = jp + + for _, opt := range opts { + opt(&jpc) + } + + return &jpc, nil +} + +func must(jpc *JpCondition, err error) *JpCondition { + if err != nil { + panic(err) + } + return jpc +} + +// MustNew returns a new JqCondition. It panics if the condition cannot be created. +func MustNew(path string, opts ...Option) *JpCondition { + return must(New(path, opts...)) +} + +// IsSatisfiedBy implements condition.Condition. +func (j *JpCondition) IsSatisfiedBy(c context.Context, resource interface{}) error { + ctx := ectx.From(c) + j.level = ctx.Level() + j.log("Evaluating jsonpath condition on resource") + value, err := j.jp.Lookup(resource) + if err != nil { + return err + } + + return j.matcher(ctx, value, resource, j.log) +} + +// Id implements types.Condition. +func (j *JpCondition) Id() string { + return j.id +} + +// Description implements types.Condition. +func (j *JpCondition) Description() string { + if j.description != "" { + return j.description + } + return j.jp.String() +} + +func defaultMatcher(ctx context.Context, value, root interface{}, log logger.Logger) error { + if value != nil { + return nil + } + return fmt.Errorf("expected non-empty result, got empty result") +} + +func (j *JpCondition) log(format string, args ...interface{}) { + s := fmt.Sprintf(format, args...) + format = "%s[%s]: %s\n" + args = []interface{}{strings.Repeat(" ", j.level), j.Description(), s} + + if j.l != nil { + j.l(format, args...) + } else { + logger.GetDefaultLogger()(format, args...) + } +} diff --git a/packages/testutils/conditions/jsonpath/jsonpath_test.go b/packages/testutils/conditions/jsonpath/jsonpath_test.go new file mode 100644 index 000000000..438abb0ea --- /dev/null +++ b/packages/testutils/conditions/jsonpath/jsonpath_test.go @@ -0,0 +1,149 @@ +package jsonpath + +import ( + "context" + "errors" + "reflect" + "testing" + + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/stretchr/testify/require" +) + +type ( + mmap = map[string]interface{} + basicTestCase struct { + name string + j *JpCondition + resource any + wantErr bool + } +) + +func TestShouldPanicForInvalidJsonPath(t *testing.T) { + require.Panics(t, func() { + MustNew(`some invalid json path`) + }) +} + +func TestIsSatisfiedBy(t *testing.T) { + type Array[T any] []T + + data := mmap{ + "store": mmap{ + "type": "book store", + "books": Array[mmap]{ + { + "title": "The Catcher in the Rye", + "author": "J.D. Salinger", + }, + { + "title": "To Kill a Mockingbird", + "author": "Harper Lee", + }, + }, + }, + } + + tests := []basicTestCase{ + { + name: "should return no error if jsonpath query matches value", + j: MustNew(`$.store.books[0].title`, WithValue("The Catcher in the Rye")), + resource: data, + }, + { + name: "should return error if jsonpath query dosesn't match value", + j: MustNew(`$.store.books[0].title`, WithValue("wrong")), + resource: data, + wantErr: true, + }, + { + name: "should return no error if jsonpath query matches for custom matcher", + j: MustNew(`$.store.books[?(@.author == 'Harper Lee')].author`, WithCustomMatcher(customAuthorMatcher)), + resource: data, + }, + { + name: "should return error if jsonpath query does not match for custom matcher", + j: MustNew(`$.store.books[?(@.author != 'Harper Lee')].author`, WithCustomMatcher(customAuthorMatcher)), + resource: data, + wantErr: true, + }, + { + name: "should return error when path resolves to nil and no matcher is specified", + j: MustNew("$.store.books[0].title.nonexistent"), + resource: data, + wantErr: true, + }, + { + name: "should return no error when path resolves to non nil and no matcher is specified", + j: MustNew("$.store.books[0].title"), + resource: data, + }, + { + name: "should accept a custom logger", + j: MustNew("$.store.books[0].title", WithLogger(customTestLogger)), + resource: data, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.j.IsSatisfiedBy(context.Background(), tt.resource) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestJsonPathCondition_Description(t *testing.T) { + tests := []struct { + name string + description string + path string + want string + }{ + { + name: "should return description if set", + description: "test description", + path: "$.foo.bar", + want: "test description", + }, + { + name: "should return path if description not set", + path: "$.foo.bar", + want: "$.foo.bar", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + j := must(New(tt.path, WithDescription(tt.description))) + + require.Contains(t, j.Description(), tt.want) + }) + } +} + +func TestHasId(t *testing.T) { + jp := MustNew(`$.foo.bar`) + require.NotEmpty(t, jp.Id()) +} + +func customAuthorMatcher(ctx context.Context, value, root interface{}, log logger.Logger) error { + fave := []interface{}{ + "Harper Lee", + } + author, ok := value.([]interface{}) + if !ok { + return errors.New("not an array") + } + if reflect.DeepEqual(author, fave) { + return nil + } + + return errors.New("jsonpath query did not match") +} + +func customTestLogger(format string, args ...interface{}) {} diff --git a/packages/testutils/doc.go b/packages/testutils/doc.go new file mode 100644 index 000000000..28aec9de2 --- /dev/null +++ b/packages/testutils/doc.go @@ -0,0 +1,122 @@ +// Package testutils provides utilities for testing. It provides 2 main interfaces: +// +// - Condition: a way to define conditions for an expectation +// - Expectation: a way to define expectations for a resource +// +// This library also includes a set of predefined conditions and expectations for most common use cases. +// +// # Conditions +// +// The condition package comes with a few implementations of the Condition interface. Some of the most important constructors are: +// - CountCondition: Creates a condition that checks if the count of a resource is equal to a given value. Works for slices and maps. +// - JqCondition: Creates a condition that checks if the result of a jq expression is valid for the resource(s). +// - JsonPathCondition: Creates a condition that checks if the result of a jsonpath expression is valid for the resource(s). +// - AllCondition: Creates a condition that checks if all of the given conditions are satisfied. +// - AnyCondition: Creates a condition that checks if any of the given conditions is satisfied. +// +// These conditions can be used to create more complex conditions. For example, the expectations.kube package has +// a predefined condition helpers to check pod readiness defined like so: +// +// var PodReadyCondition types.Condition = conditions.All( +// conditions.NewKubernetesStatusCondition("Ready", true), +// conditions.NewKubernetesStatusCondition("Initialized", true), +// conditions.NewKubernetesStatusCondition("ContainersReady", true), +// ) +// +// It uses the NewKubernetesStatusCondition constructor (which itself uses the jsonpath condition constructor) +// to create a conditions that checks if the status of a pod is equal to a given value. Then it combines all of these conditions +// using the AllCondition constructor. +// +// # Expectations +// +// The epectation package comes with 4 implementations of the Expectation interface: +// - KubernetesExpectation: an expectation for kubernetes resources +// - HelmExpectation: an expectation for helm releases +// - AllExpectation: an expectation for grouping multiple expectations and checking if all of them are satisfied +// - AnyExpectation: an expectation for grouping multiple expectations and checking if any of them is satisfied +// +// # KubernetesExpectation +// +// The KubernetesExpectation is an expectation for kubernetes resources. +// It is satisfied if the resource is present or not present in the cluster (depending on its configuration). +// The main constructor of this expectation `kube.Resource` has 3 required parameters: +// - pattern: a regex pattern string that matches the name of the expected resource(s) +// - namespace: the namespace of the expected resource(s). This is parameter is ignored if the resource is cluster-scoped. +// If this resource should be matched in all namespaces, use the "*" wildcard. +// - gvk: the group, version, kind of the expected resource(s) +// +// The expectation also accepts a list of options that can be used to configure the expectation. +// See the options section of the package documentation for more details. +// +// Because some resources are commonly expected in the cluster, this package also provides a set of predefined expectations +// and constructors for them: +// - kube.AbsentResource: a constructor for an expectation for a resource that is not present in the cluster. ie: a resource that has been deleted or has a count of 0. +// - kube.Pod: an expectation for a pod(s) in the cluster +// - kube.AbsentPod: an expectation for a pod(s) that is not present in the cluster +// - kube.Target: an expectation for a target(s) in the cluster +// - kube.Solution: an expectation for a solution(s) in the cluster +// - kube.Instance: an expectation for an instance(s) in the cluster +// +// # HelmExpectation +// +// The HelmExpectation is an expectation for helm releases. It is satisfied if the release is present or not present in the cluster (depending on its configuration). +// The main constructor of this expectation `helm.New` has 2 required parameters: +// - pattern: a regex pattern string that matches the name of the expected release(s) +// - namespace: the namespace of the expected release(s). To match releases in all namespaces, use the "*" wildcard. +// +// The expectation also accepts a list of options that can be used to configure the expectation. See the options section of the package documentation for more details. +// +// # AllExpectation +// +// The AllExpectation is an expectation for grouping multiple expectations and checking if all of them are satisfied. +// The main constructor of this expectation `expectations.All` accepts a list of expectations as parameters. +// +// # AnyExpectation +// +// The AnyExpectation is an expectation for grouping multiple expectations and checking if any of them is satisfied. +// The main constructor of this expectation `expectations.Any` accepts a list of expectations as parameters. +// +// # Examples +// +// Check if a pod named "my-pod-34jfk3-fd4k56g" in namespace "default" exists in the cluster: +// +// exp := kube.Must(kube.Pod("my-pod-34jfk3-fd4k56g", "default")) +// if err := exp.Verify(ctx); err != nil { +// // expectation failed. handle error +// } +// +// Check if there are 2 pods with prefix "my-pod-" in namespace "default" in the cluster and they are ready: +// +// exp := kube.Must(kube.Pod( +// "my-pod-.*", +// "default", +// kube.WithListCondition( +// conditions.Count(2) +// ), +// kube.WithCondition(kube.PodReadyCondition), // PodReadyCondition is pre-defined in the kube package +// )) +// if err := exp.Verify(ctx); err != nil { +// // expectation failed. handle error +// } +// +// Check if there are 2 pods with prefix "my-pod-" in namespace "default" in the cluster +// and that each pod is ready or initialized and each pod has a specific label +// +// exp := kube.Must(kube.Pod( +// "my-pod-.*", +// "default", +// kube.WithListCondition( +// conditions.Count(2) +// ), +// kube.WithCondition(conditions.All( +// conditions.Any( +// conditions.NewKubernetesStatusCondition("Ready", true), +// conditions.NewKubernetesStatusCondition("Initialized", true), +// ), +// kube.NewLabelMatchCondition("my-label", "my-value"), +// )), +// )) +// if err := exp.Verify(ctx); err != nil { +// // expectation failed. handle error +// } +package main diff --git a/packages/testutils/expectations/README.md b/packages/testutils/expectations/README.md new file mode 100644 index 000000000..ea39b1dcf --- /dev/null +++ b/packages/testutils/expectations/README.md @@ -0,0 +1,127 @@ + + +# expectations + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/expectations" +``` + +## Index + +- [type AllExpectation](<#AllExpectation>) + - [func All\(expectations ...types.Expectation\) \*AllExpectation](<#All>) + - [func \(e \*AllExpectation\) Description\(\) string](<#AllExpectation.Description>) + - [func \(e \*AllExpectation\) Id\(\) string](<#AllExpectation.Id>) + - [func \(e \*AllExpectation\) Verify\(c context.Context\) error](<#AllExpectation.Verify>) + - [func \(a \*AllExpectation\) WithCaching\(\) \*AllExpectation](<#AllExpectation.WithCaching>) +- [type AnyExpectation](<#AnyExpectation>) + - [func Any\(expectations ...types.Expectation\) \*AnyExpectation](<#Any>) + - [func \(e \*AnyExpectation\) Description\(\) string](<#AnyExpectation.Description>) + - [func \(e \*AnyExpectation\) Id\(\) string](<#AnyExpectation.Id>) + - [func \(e \*AnyExpectation\) Verify\(c context.Context\) error](<#AnyExpectation.Verify>) + + + +## type [AllExpectation]() + + + +```go +type AllExpectation struct { + // contains filtered or unexported fields +} +``` + + +### func [All]() + +```go +func All(expectations ...types.Expectation) *AllExpectation +``` + +All returns an expectation that is satisfied if all of the given expectations are satisfied. + + +### func \(\*AllExpectation\) [Description]() + +```go +func (e *AllExpectation) Description() string +``` + +Description implements types.Expectation. + + +### func \(\*AllExpectation\) [Id]() + +```go +func (e *AllExpectation) Id() string +``` + +Id implements types.Expectation. + + +### func \(\*AllExpectation\) [Verify]() + +```go +func (e *AllExpectation) Verify(c context.Context) error +``` + +Verify implements types.Expectation. + + +### func \(\*AllExpectation\) [WithCaching]() + +```go +func (a *AllExpectation) WithCaching() *AllExpectation +``` + +WithCaching returns a new expectation that caches the result of each expectation successfull expectation so that it is not verified again in future calls to Verify. + + +## type [AnyExpectation]() + + + +```go +type AnyExpectation struct { + // contains filtered or unexported fields +} +``` + + +### func [Any]() + +```go +func Any(expectations ...types.Expectation) *AnyExpectation +``` + +Any returns an expectation that is satisfied if any of the given expectations is satisfied. + + +### func \(\*AnyExpectation\) [Description]() + +```go +func (e *AnyExpectation) Description() string +``` + +Description implements types.Expectation. + + +### func \(\*AnyExpectation\) [Id]() + +```go +func (e *AnyExpectation) Id() string +``` + +Id implements types.Expectation. + + +### func \(\*AnyExpectation\) [Verify]() + +```go +func (e *AnyExpectation) Verify(c context.Context) error +``` + +Verify implements types.Expectation. + +Generated by [gomarkdoc]() diff --git a/packages/testutils/expectations/expectation.go b/packages/testutils/expectations/expectation.go new file mode 100644 index 000000000..7224d0469 --- /dev/null +++ b/packages/testutils/expectations/expectation.go @@ -0,0 +1,126 @@ +package expectations + +import ( + "context" + "fmt" + "strings" + + econtext "github.com/eclipse-symphony/symphony/packages/testutils/internal/context" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/google/uuid" +) + +type ( + AllExpectation struct { + expectations []types.Expectation + successCache map[string]struct{} + shouldCache bool + level int + id string + } + AnyExpectation struct { + level int + id string + expectations []types.Expectation + } +) + +var ( + _ types.Expectation = &AllExpectation{} + _ types.Expectation = &AnyExpectation{} +) + +// Verify implements types.Expectation. +func (e *AnyExpectation) Verify(c context.Context) error { + ctx := econtext.From(c) + e.level = ctx.Level() + e.log("checking if any expectation is satisfied") + for i, expectation := range e.expectations { + e.log("checking expectation %d of %d: [%s]", i+1, len(e.expectations), expectation.Description()) + if err := expectation.Verify(ctx.Nested()); err == nil { + e.log("expectation %d of %d was satisfied", i+1, len(e.expectations)) + return nil + } + } + return fmt.Errorf("no expectation was satisfied") +} + +// Verify implements types.Expectation. +func (e *AllExpectation) Verify(c context.Context) error { + ctx := econtext.From(c) + e.level = ctx.Level() + e.log("checking if all expectations are satisfied") + for i, expectation := range e.expectations { + if _, ok := e.successCache[expectation.Id()]; ok && e.shouldCache { + e.log("expectation %d of %d was satisfied (cached) [%s]: skipping...", i+1, len(e.expectations), expectation.Description()) + continue + } + e.log("checking expectation %d of %d: [%s]", i+1, len(e.expectations), expectation.Description()) + if err := expectation.Verify(ctx.Nested()); err != nil { + e.log("expectation %d of %d failed: %s", i+1, len(e.expectations), err) + return err + } + e.log("expectation %d of %d was satisfied [%s]", i+1, len(e.expectations), expectation.Description()) + if e.shouldCache { + e.successCache[expectation.Id()] = struct{}{} + } + } + e.log("all expectations were satisfied") + return nil +} + +// Description implements types.Expectation. +func (e *AnyExpectation) Description() string { + return "any expectation" +} + +// Id implements types.Expectation. +func (e *AnyExpectation) Id() string { + return e.id +} + +// Id implements types.Expectation. +func (e *AllExpectation) Id() string { + return e.id +} + +// Description implements types.Expectation. +func (e *AllExpectation) Description() string { + return "all expectation" +} + +// WithCaching returns a new expectation that caches the result of each expectation successfull expectation +// so that it is not verified again in future calls to Verify. +func (a *AllExpectation) WithCaching() *AllExpectation { + na := All(a.expectations...) + na.shouldCache = true + return na +} + +// All returns an expectation that is satisfied if all of the given expectations are satisfied. +func All(expectations ...types.Expectation) *AllExpectation { + return &AllExpectation{ + id: uuid.NewString(), + expectations: expectations, + successCache: make(map[string]struct{}), + } +} + +// Any returns an expectation that is satisfied if any of the given expectations is satisfied. +func Any(expectations ...types.Expectation) *AnyExpectation { + return &AnyExpectation{ + id: uuid.NewString(), + expectations: expectations, + } +} + +func (e *AnyExpectation) log(str string, args ...interface{}) { + s := fmt.Sprintf(str, args...) + logger.GetDefaultLogger()("%s[%s]: %s\n", strings.Repeat(" ", e.level), e.Description(), s) +} + +func (e *AllExpectation) log(str string, args ...interface{}) { + s := fmt.Sprintf(str, args...) + logger.GetDefaultLogger()("%s[%s]: %s\n", strings.Repeat(" ", e.level), e.Description(), s) +} diff --git a/packages/testutils/expectations/expectation_test.go b/packages/testutils/expectations/expectation_test.go new file mode 100644 index 000000000..23e5108e0 --- /dev/null +++ b/packages/testutils/expectations/expectation_test.go @@ -0,0 +1,228 @@ +package expectations + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +type ( + basicConditionTest struct { + name string + expectation types.Expectation + wantErr bool + } +) + +func TestAll(t *testing.T) { + tests := []basicConditionTest{ + { + name: "should not return error if all expectations are satisfied - 1 right", + expectation: All(expectation(true)), + }, + { + name: "should not return error if all expectations are satisfied - 2 right", + expectation: All(expectation(true), expectation(true)), + }, + { + name: "should return an error if any expectation is not satisfied - 1 wrong", + expectation: All(expectation(false)), + wantErr: true, + }, + { + name: "should return an error if any expectation is not satisfied - 2 wrong", + expectation: All(expectation(false), expectation(false)), + wantErr: true, + }, + { + name: "should return an error if all expectations are not satisfied - 1 right, 1 wrong", + expectation: All(expectation(true), expectation(false)), + wantErr: true, + }, + { + name: "should return an error when any expectation is not satisfied - nested", + expectation: All( + All(expectation(true)), + All( + expectation(true), + expectation(true), + ), + All( + expectation(true), + All( + expectation(false), // this one fails + ), + ), + ), + wantErr: true, + }, + { + name: "should return not return error when all expectations are satisfied - nested", + expectation: All( + All(expectation(true)), + All( + expectation(true), + expectation(true), + ), + All( + expectation(true), + All( + expectation(true), + ), + ), + ), + }, + } + testBasicConditions(t, tests) +} + +func TestAny(t *testing.T) { + tests := []basicConditionTest{ + { + name: "should not return error if any expectation is satisfied - 1 right", + expectation: Any(expectation(true)), + }, + { + name: "should not return error if any expectation is satisfied - 2 right", + expectation: Any(expectation(true), expectation(true)), + }, + { + name: "should return an error if no expectation is satisfied - 1 wrong", + expectation: Any(expectation(false)), + wantErr: true, + }, + { + name: "should return an error if no expectation is satisfied - 2 wrong", + expectation: Any(expectation(false), expectation(false)), + wantErr: true, + }, + { + name: "should not return an error if any expectation is satisfied - 1 right, 1 wrong", + expectation: Any(expectation(false), expectation(true)), + }, + } + testBasicConditions(t, tests) +} + +func TestCombo(t *testing.T) { + tests := []basicConditionTest{ + { + name: "combo - nested", + expectation: Any( + Any(expectation(false)), //false + Any( + expectation(false), // false + expectation(false), // false + ), // false + All( + expectation(true), // true + Any( + expectation(false), // false + expectation(true), // true + ), // true + ), // true + ), // true + }, + { + name: "should return error for combo", + expectation: Any( + Any(expectation(false)), //false + All( + expectation(false), // false + expectation(true), // true + ), // false + Any( + expectation(false), // false + Any( + expectation(false), // false + ), // false + ), // false + ), // false + wantErr: true, + }, + } + testBasicConditions(t, tests) +} + +func TestAllWithCache(t *testing.T) { + passingExpectation := expectation(true) + failingExpectation := expectation(false) + c := All( + passingExpectation, + failingExpectation, + ).WithCaching() + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + defer cancel() + + require.Equal(t, 0, passingExpectation.callexpectations["Verify"]) + require.Equal(t, 0, failingExpectation.callexpectations["Verify"]) + require.Error(t, c.Verify(ctx)) + + require.Equal(t, 1, passingExpectation.callexpectations["Verify"]) + require.Equal(t, 1, failingExpectation.callexpectations["Verify"]) + + require.Error(t, c.Verify(ctx)) + + require.Equal(t, 1, passingExpectation.callexpectations["Verify"]) + require.Equal(t, 2, failingExpectation.callexpectations["Verify"]) +} + +func testBasicConditions(t *testing.T, tt []basicConditionTest) { + for _, tt := range tt { + t.Run(tt.name, func(t *testing.T) { + require.NotEmpty(t, tt.expectation.Id()) + require.NotEmpty(t, tt.expectation.Description()) + + err := tt.expectation.Verify(context.Background()) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +type mockExpectation struct { + id string + callexpectations map[string]int + shouldPass bool +} + +// Description implements Expectation. +func (m mockExpectation) Description() string { + defer m.call("Description") + return "testing expectation" +} + +// Id implements Expectation. +func (m mockExpectation) Id() string { + defer m.call("Id") + return m.id +} + +// Verify implements Expectation. +func (m mockExpectation) Verify(ctx context.Context) error { + defer m.call("Verify") + if !m.shouldPass { + return errors.New("mock expectation failed") + } + return nil +} + +func (m mockExpectation) call(n string) { + m.callexpectations[n]++ +} + +func expectation(shouldPass bool) mockExpectation { + return mockExpectation{ + id: uuid.NewString(), + shouldPass: shouldPass, + callexpectations: make(map[string]int), + } +} diff --git a/packages/testutils/expectations/helm/README.md b/packages/testutils/expectations/helm/README.md new file mode 100644 index 000000000..80ba90333 --- /dev/null +++ b/packages/testutils/expectations/helm/README.md @@ -0,0 +1,219 @@ + + +# helm + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/expectations/helm" +``` + +## Index + +- [Variables](<#variables>) +- [type HelmExpectation](<#HelmExpectation>) + - [func MustNew\(name, namespace string, opts ...Option\) \*HelmExpectation](<#MustNew>) + - [func MustNewAbsent\(name, namespace string, opts ...Option\) \*HelmExpectation](<#MustNewAbsent>) + - [func NewExpectation\(pattern, namespace string, opts ...Option\) \(\*HelmExpectation, error\)](<#NewExpectation>) + - [func \(e \*HelmExpectation\) AsGomegaSubject\(\) func\(context.Context\) \(interface\{\}, error\)](<#HelmExpectation.AsGomegaSubject>) + - [func \(he \*HelmExpectation\) Description\(\) string](<#HelmExpectation.Description>) + - [func \(he \*HelmExpectation\) Id\(\) string](<#HelmExpectation.Id>) + - [func \(e \*HelmExpectation\) ToGomegaMatcher\(\) gomega.GomegaMatcher](<#HelmExpectation.ToGomegaMatcher>) + - [func \(he \*HelmExpectation\) Verify\(c context.Context\) error](<#HelmExpectation.Verify>) +- [type ListRunner](<#ListRunner>) +- [type Option](<#Option>) + - [func WithDescription\(description string\) Option](<#WithDescription>) + - [func WithListClientBuilder\(builder func\(\) \(ListRunner, error\)\) Option](<#WithListClientBuilder>) + - [func WithLogger\(logger func\(format string, args ...interface\{\}\)\) Option](<#WithLogger>) + - [func WithReleaseCondition\(condition types.Condition\) Option](<#WithReleaseCondition>) + - [func WithReleaseListCondition\(condition types.Condition\) Option](<#WithReleaseListCondition>) + - [func WithRemoved\(removed bool\) Option](<#WithRemoved>) + - [func WithValueCondition\(condition types.Condition\) Option](<#WithValueCondition>) + - [func WithValueListCondition\(condition types.Condition\) Option](<#WithValueListCondition>) + + +## Variables + + + +```go +var ( + DeployedCondition = jq.Equality(".info.status", "deployed") + FailedCondition = jq.Equality(".info.status", "failed") +) +``` + + +## type [HelmExpectation]() + + + +```go +type HelmExpectation struct { + // contains filtered or unexported fields +} +``` + + +### func [MustNew]() + +```go +func MustNew(name, namespace string, opts ...Option) *HelmExpectation +``` + +MustNew creates a new helm expectation. It panics if the expectation cannot be created. + + +### func [MustNewAbsent]() + +```go +func MustNewAbsent(name, namespace string, opts ...Option) *HelmExpectation +``` + +NewPresent creates a new helm expectation that expects the release to be present. + + +### func [NewExpectation]() + +```go +func NewExpectation(pattern, namespace string, opts ...Option) (*HelmExpectation, error) +``` + +NewExpectation creates a new helm expectation. + + +### func \(\*HelmExpectation\) [AsGomegaSubject]() + +```go +func (e *HelmExpectation) AsGomegaSubject() func(context.Context) (interface{}, error) +``` + + + + +### func \(\*HelmExpectation\) [Description]() + +```go +func (he *HelmExpectation) Description() string +``` + + + + +### func \(\*HelmExpectation\) [Id]() + +```go +func (he *HelmExpectation) Id() string +``` + +Id implements types.Expectation. + + +### func \(\*HelmExpectation\) [ToGomegaMatcher]() + +```go +func (e *HelmExpectation) ToGomegaMatcher() gomega.GomegaMatcher +``` + + + + +### func \(\*HelmExpectation\) [Verify]() + +```go +func (he *HelmExpectation) Verify(c context.Context) error +``` + +Verify implements types.Expectation. + + +## type [ListRunner]() + + + +```go +type ListRunner interface { + Run() ([]*release.Release, error) +} +``` + + +## type [Option]() + + + +```go +type Option func(*HelmExpectation) +``` + + +### func [WithDescription]() + +```go +func WithDescription(description string) Option +``` + + + + +### func [WithListClientBuilder]() + +```go +func WithListClientBuilder(builder func() (ListRunner, error)) Option +``` + + + + +### func [WithLogger]() + +```go +func WithLogger(logger func(format string, args ...interface{})) Option +``` + + + + +### func [WithReleaseCondition]() + +```go +func WithReleaseCondition(condition types.Condition) Option +``` + + + + +### func [WithReleaseListCondition]() + +```go +func WithReleaseListCondition(condition types.Condition) Option +``` + + + + +### func [WithRemoved]() + +```go +func WithRemoved(removed bool) Option +``` + +WithRemoved specifies whether the release is expected to be present or not. + + +### func [WithValueCondition]() + +```go +func WithValueCondition(condition types.Condition) Option +``` + + + + +### func [WithValueListCondition]() + +```go +func WithValueListCondition(condition types.Condition) Option +``` + + + +Generated by [gomarkdoc]() diff --git a/packages/testutils/expectations/helm/commons.go b/packages/testutils/expectations/helm/commons.go new file mode 100644 index 000000000..42ef089d1 --- /dev/null +++ b/packages/testutils/expectations/helm/commons.go @@ -0,0 +1,8 @@ +package helm + +import "github.com/eclipse-symphony/symphony/packages/testutils/conditions/jq" + +var ( + DeployedCondition = jq.Equality(".info.status", "deployed") + FailedCondition = jq.Equality(".info.status", "failed") +) diff --git a/packages/testutils/expectations/helm/gomega.go b/packages/testutils/expectations/helm/gomega.go new file mode 100644 index 000000000..249337ddd --- /dev/null +++ b/packages/testutils/expectations/helm/gomega.go @@ -0,0 +1,32 @@ +package helm + +import ( + "context" + "fmt" + + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/onsi/gomega/gcustom" + gomega "github.com/onsi/gomega/types" + "helm.sh/helm/v3/pkg/release" +) + +var _ types.GomegaEventuallySubject = &HelmExpectation{} + +func (e *HelmExpectation) AsGomegaSubject() func(context.Context) (interface{}, error) { + return func(c context.Context) (interface{}, error) { + return e.getResults(c) + } +} + +func (e *HelmExpectation) ToGomegaMatcher() gomega.GomegaMatcher { + return gcustom.MakeMatcher(func(resource interface{}) (bool, error) { + releases, ok := resource.([]*release.Release) + if !ok { + return false, fmt.Errorf("expected resource to be a list of release.Release, got %T", resource) + } + if err := e.verifyConditions(context.TODO(), releases); err != nil { + return false, nil + } + return true, nil + }) +} diff --git a/packages/testutils/expectations/helm/options.go b/packages/testutils/expectations/helm/options.go new file mode 100644 index 000000000..c9ecfc859 --- /dev/null +++ b/packages/testutils/expectations/helm/options.go @@ -0,0 +1,53 @@ +package helm + +import "github.com/eclipse-symphony/symphony/packages/testutils/types" + +// WithRemoved specifies whether the release is expected to be present or not. +func WithRemoved(removed bool) Option { + return func(h *HelmExpectation) { + h.removed = removed + } +} + +func WithListClientBuilder(builder func() (ListRunner, error)) Option { + return func(h *HelmExpectation) { + h.actionBuilder = builder + } +} + +func WithValueListCondition(condition types.Condition) Option { + return func(h *HelmExpectation) { + newC := createValueConditionFrom(condition, true) + addCondition(&h.releaseListCondition, newC) + } +} +func WithValueCondition(condition types.Condition) Option { + return func(h *HelmExpectation) { + newC := createValueConditionFrom(condition, false) + addCondition(&h.releaseCondition, newC) + } +} + +func WithReleaseCondition(condition types.Condition) Option { + return func(h *HelmExpectation) { + addCondition(&h.releaseCondition, condition) + } +} + +func WithReleaseListCondition(condition types.Condition) Option { + return func(h *HelmExpectation) { + addCondition(&h.releaseListCondition, condition) + } +} + +func WithDescription(description string) Option { + return func(h *HelmExpectation) { + h.description = description + } +} + +func WithLogger(logger func(format string, args ...interface{})) Option { + return func(h *HelmExpectation) { + h.l = logger + } +} diff --git a/packages/testutils/expectations/helm/resource.go b/packages/testutils/expectations/helm/resource.go new file mode 100644 index 000000000..716e2ece6 --- /dev/null +++ b/packages/testutils/expectations/helm/resource.go @@ -0,0 +1,311 @@ +package helm + +import ( + "context" + "encoding/json" + "fmt" + "os" + "regexp" + "strings" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/conditions" + "github.com/eclipse-symphony/symphony/packages/testutils/helpers" + ectx "github.com/eclipse-symphony/symphony/packages/testutils/internal/context" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/google/uuid" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" +) + +type ( + ListRunner interface { + Run() ([]*release.Release, error) + } + HelmExpectation struct { + // pattern is the pattern of the release + pattern string + + // description is a friendly description of the expectation + description string + + // removed indicates whether the release is expected to be present or not + removed bool + + // namespace is the namespace of the release + namespace string + + // releaseCondition specifies the types.that the release should satisfy + releaseCondition types.Condition + releaseListCondition types.Condition + + action ListRunner + actionBuilder func() (ListRunner, error) + l func(format string, args ...interface{}) + tick time.Duration + timeout time.Duration + nameRegex *regexp.Regexp + level int + id string + initialised bool + } + + Option func(*HelmExpectation) +) + +const ( + defaultTick = 5 * time.Second + defaultTimeout = 5 * time.Minute +) + +var _ types.Expectation = &HelmExpectation{} + +// NewExpectation creates a new helm expectation. +func NewExpectation(pattern, namespace string, opts ...Option) (*HelmExpectation, error) { + if namespace == "" { + return nil, fmt.Errorf("namespace cannot be empty") + } + + he := &HelmExpectation{ + pattern: boundPattern(pattern), + namespace: namespace, + tick: defaultTick, + timeout: defaultTimeout, + id: uuid.NewString(), + } + he.actionBuilder = getDefaultActionBuilder(namespace, he.log) + + for _, opt := range opts { + opt(he) + } + + nameRegex, err := regexp.Compile(he.pattern) + if err != nil { + return nil, err + } + he.nameRegex = nameRegex + + he.initializeCountCondition() + + return he, nil +} + +// MustNew creates a new helm expectation. It panics if the expectation cannot be created. +func MustNew(name, namespace string, opts ...Option) *HelmExpectation { + he, err := NewExpectation(name, namespace, opts...) + if err != nil { + panic(err) + } + return he +} + +// NewPresent creates a new helm expectation that expects the release to be present. +func MustNewAbsent(name, namespace string, opts ...Option) *HelmExpectation { + return MustNew(name, namespace, append(opts, WithRemoved(true))...) +} + +func (he *HelmExpectation) initAction() error { + if he.initialised { + return nil + } + action, err := he.actionBuilder() + if err != nil { + return err + } + he.action = action + he.initialised = true + return nil +} + +func (he *HelmExpectation) log(format string, args ...interface{}) { + s := fmt.Sprintf(format, args...) + format = "%s[%s]: %s\n" + args = []interface{}{strings.Repeat(" ", he.level), he.Description(), s} + + if he.l != nil { + he.l(format, args...) + } else { + logger.GetDefaultLogger()(format, args...) + } +} + +// Verify implements types.Expectation. +func (he *HelmExpectation) Verify(c context.Context) error { + ctx := ectx.From(c) + he.level = ctx.Level() + + return helpers.Eventually(ctx, func(ctx context.Context) (err error) { + he.log(strings.Repeat("-", 80)) + he.log("Verifying helm release") + defer func() { + if err != nil { + he.log("Error while verifying helm release: %s", err) + } + }() + + matches, err := he.getResults(ctx) + if err != nil { + return + } + if err = he.verifyConditions(ctx, matches); err != nil { + return + } + return nil + }, he.tick, "Timed out while verifying helm release %s", he.Description()) +} + +// Id implements types.Expectation. +func (he *HelmExpectation) Id() string { + return he.id +} + +func (re *HelmExpectation) initializeCountCondition() { + countCondition := conditions.GreaterThan(0) + if re.removed { + countCondition = conditions.Count(0) + } + + addCondition(&re.releaseListCondition, countCondition) +} + +func (he *HelmExpectation) getResults(ctx context.Context) ([]*release.Release, error) { + if err := he.initAction(); err != nil { + return nil, err + } + releases, err := he.action.Run() + if err != nil { + return nil, err + } + he.log("Found %d releases", len(releases)) + he.log("Action Type: %T", he.action) + matches := he.getMatches(releases) + he.log("Found %d matching releases", len(matches)) + return matches, nil +} + +func (he *HelmExpectation) verifyConditions(ctx context.Context, releases []*release.Release) error { + //Todo: Add support for list conditions + he.log("Verifying conditions") + arr := make([]map[string]interface{}, len(releases)) + b, err := json.Marshal(releases) + if err != nil { + return err + } + json.Unmarshal(b, &arr) + + if err := he.verifyListCondition(ctx, arr); err != nil { + return err + } + + if err := he.verifyUnitCondition(ctx, arr); err != nil { + return err + } + + return nil +} + +func (he *HelmExpectation) verifyUnitCondition(c context.Context, releases []map[string]interface{}) error { + ctx := ectx.From(c) + if he.releaseCondition != nil { + for _, release := range releases { + if err := he.releaseCondition.IsSatisfiedBy(ctx.Nested(), release); err != nil { + return err + } + } + } + return nil +} + +func (he *HelmExpectation) verifyListCondition(c context.Context, releases []map[string]interface{}) error { + ctx := ectx.From(c) + if he.releaseListCondition != nil { + return he.releaseListCondition.IsSatisfiedBy(ctx.Nested(), releases) + } + return nil +} + +func (he *HelmExpectation) getMatches(releases []*release.Release) (matches []*release.Release) { + for i := range releases { + if he.nameRegex.MatchString(releases[i].Name) { + matches = append(matches, releases[i]) + } + } + return matches +} + +func (he *HelmExpectation) Description() string { + if he.description != "" { + return he.description + } + return fmt.Sprintf("helm expectation: %s", he.pattern) +} + +func getDefaultActionBuilder(namespace string, logger logger.Logger) func() (ListRunner, error) { + return func() (ListRunner, error) { + settings := cli.New() + var allNamespaces bool + + if namespace == "*" { + allNamespaces = true + } else { + settings.SetNamespace(namespace) + } + + actionConfig := new(action.Configuration) + actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), logger) + action := action.NewList(actionConfig) + + action.AllNamespaces = allNamespaces + return action, nil + } +} + +func boundPattern(str string) string { + if !strings.HasPrefix(str, "^") { + str = "^" + str + } + if !strings.HasSuffix(str, "$") { + str = str + "$" + } + return str +} + +func createValueConditionFrom(condition types.Condition, isListCondition bool) types.Condition { + return conditions.Basic(func(ctx context.Context, i interface{}) error { + if isListCondition { + switch releases := i.(type) { + case []map[string]any: + values := make([]map[string]interface{}, len(releases)) + for i, release := range releases { + val, _ := release["config"].(map[string]interface{}) + values[i] = val + } + return condition.IsSatisfiedBy(ctx, values) + default: + return fmt.Errorf("expected []interface{}, got %T", i) + } + } + switch release := i.(type) { + case map[string]interface{}: + value, _ := release["config"].(map[string]interface{}) + return condition.IsSatisfiedBy(ctx, value) + + default: + return fmt.Errorf("expected map[string]interface{}, got %T", i) + } + }, conditions.WithBasicDescription("values check")) +} + +func addCondition(existingCondition *types.Condition, newCondition types.Condition) { + if *existingCondition != nil { + if c, ok := (*existingCondition).(interface { + And(...types.Condition) types.Condition + }); ok { + *existingCondition = c.And(newCondition) + } + } else { + *existingCondition = conditions.All(newCondition) + } +} diff --git a/packages/testutils/expectations/helm/resource_test.go b/packages/testutils/expectations/helm/resource_test.go new file mode 100644 index 000000000..ab4046e84 --- /dev/null +++ b/packages/testutils/expectations/helm/resource_test.go @@ -0,0 +1,309 @@ +package helm + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/conditions" + "github.com/eclipse-symphony/symphony/packages/testutils/conditions/jq" + "github.com/eclipse-symphony/symphony/packages/testutils/internal" + "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/release" +) + +type ( + mockLister struct { + count int + shoudError bool + } +) + +var ( + _ ListRunner = mockLister{} + testTimeout = time.Millisecond * 5 +) + +func TestNoErrorForNewResourceValidator(t *testing.T) { + e, err := NewExpectation("name", "*", + WithListClientBuilder(getMockListerBuilder(1, false)), + WithDescription("description"), + ) + + require.NoError(t, err) + require.NotEmpty(t, e.Description()) + require.NotEmpty(t, e.Id()) + +} + +func TestErrorForNewResourceWithIncorrectConfig(t *testing.T) { + _, err := NewExpectation("", "") // no pattern or namespace + require.Error(t, err) +} + +func TestErrorForNewResourceWithInvalidNameValidator(t *testing.T) { + _, err := NewExpectation("( ", "namespaace") + + require.Error(t, err) +} + +func TestShouldPanicForInvalidName(t *testing.T) { + require.Panics(t, func() { + MustNew("( ", "namespaace") + }) +} + +func TestShouldErrorWhenListFails(t *testing.T) { + e, err := NewExpectation("name", "namespace", WithListClientBuilder(getMockListerBuilder(0, true))) + + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + err = e.Verify(ctx) + + require.Error(t, err) +} + +func TestShouldFindMatchingReleaseFromExactName(t *testing.T) { + e, err := NewExpectation("release-0", "namespace", + WithListClientBuilder(getMockListerBuilder(1, false)), + WithReleaseCondition(jq.MustNew(`.chart.metadata.name`, jq.WithValue("chart-0"))), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestShouldFailOnMatchButFalseReleaseCondition(t *testing.T) { + e, err := NewExpectation("release-0", "namespace", + WithLogger(func(format string, args ...interface{}) {}), + WithListClientBuilder(getMockListerBuilder(1, false)), + WithReleaseCondition(jq.MustNew(`.chart.metadata.name`, jq.WithValue("wrong"))), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestShouldFailOnMatchButFalseValueCondition(t *testing.T) { + e, err := NewExpectation("release-0", "namespace", + WithListClientBuilder(getMockListerBuilder(1, false)), + WithValueCondition(jq.MustNew(`.foo`, jq.WithValue("baz"))), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestShouldFindMatchingReleaseFromExactNameAndExtraConditions(t *testing.T) { + e, err := NewExpectation("release-0", "namespace", + WithListClientBuilder(getMockListerBuilder(1, false)), + WithReleaseCondition(conditions.All( + jq.Equality(`.chart.metadata.name`, "chart-0"), + jq.Equality(`.chart.metadata.version`, "x.y.z"), + )), + WithReleaseListCondition(conditions.Count(1)), + WithValueCondition(conditions.All( + jq.Equality(`.foo`, "bar"), + jq.Equality(`.baz`, "qux"), + )), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestShouldNotFindMatchingReleaseWithUnexactName(t *testing.T) { + e, err := NewExpectation("release", "namespace", // No release with name "release" + WithListClientBuilder(getMockListerBuilder(1, false)), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} +func TestShouldMatchAbsentRelease(t *testing.T) { + e := MustNewAbsent("some-non-existent-release", "*", // No release with name "some-non-existent-release" in any namespace + WithListClientBuilder(getMockListerBuilder(1, false)), + ) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestShouldFindMultipleReleases(t *testing.T) { + e, err := NewExpectation("release-.+", "namespace", // regex to match all releases in namespace + WithListClientBuilder(getMockListerBuilder(5, false)), + WithReleaseListCondition( + conditions.Count(5), + ), + WithValueListCondition( + conditions.Count(5), + ), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestShouldFindMultipleReleasesButFailForIncorrectReleaseCondtion(t *testing.T) { + e, err := NewExpectation("release-.+", "namespace", // regex to match all releases in namespace + WithListClientBuilder(getMockListerBuilder(10, false)), + WithReleaseListCondition( + conditions.Count(5), // correct count is 10 + + ), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestShouldFindMultipleReleasesButFailForIncorrectValueCondtion(t *testing.T) { + e, err := NewExpectation("release-.+", "namespace", // regex to match all releases in namespace + WithListClientBuilder(getMockListerBuilder(10, false)), + WithValueListCondition( + conditions.Count(5), // correct count is 10 + ), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestVerifyPasses(t *testing.T) { + e := MustNew("release-0", "namespace", + WithListClientBuilder(getMockListerBuilder(1, false)), + WithReleaseCondition(jq.MustNew(`.chart.metadata.name`, jq.WithValue("chart-0"))), + ) + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) + defer cancel() + + require.NoError(t, e.Verify(ctx)) +} + +func TestVerifyFailsWhenContextEnds(t *testing.T) { + e := MustNew("release-0", "namespace", + WithListClientBuilder(getMockListerBuilder(1, false)), + WithReleaseCondition(jq.MustNew(`.chart.metadata.name`, jq.WithValue("wrong"))), + ) + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) + defer cancel() + + require.Error(t, e.Verify(ctx)) +} + +func TestWhenListBuilderInitializeFails(t *testing.T) { + ex, err := NewExpectation("release-0", "namespace", + WithListClientBuilder(func() (ListRunner, error) { + return nil, fmt.Errorf("some error") + }), + WithReleaseCondition(jq.MustNew(`.chart.metadata.name`, jq.WithValue("wrong"))), + ) + + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) + defer cancel() + + require.Error(t, ex.Verify(ctx)) +} + +func TestWorksWithGomegaAssertion(t *testing.T) { + mt := internal.NewMockT() + g := gomega.NewGomegaWithT(mt) + e, err := NewExpectation("release-0", "namespace", + WithListClientBuilder(getMockListerBuilder(1, false)), + WithReleaseCondition(conditions.All( + jq.Equality(`.chart.metadata.name`, "chart-0"), + jq.Equality(`.chart.metadata.version`, "x.y.z"), + )), // false runs the condiition on individual resources + WithReleaseListCondition(conditions.Count(1)), + WithValueCondition(conditions.All( + jq.Equality(`.foo`, "bar"), + jq.Equality(`.baz`, "qux"), + )), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + mt.On("Helper").Return() + g.Eventually(e.AsGomegaSubject()).WithContext(ctx).Should(e.ToGomegaMatcher()) + mt.AssertExpectations(t) + + mt.On("Fatalf", mock.Anything, mock.Anything).Return() + g.Eventually(e.AsGomegaSubject()).WithContext(ctx).ShouldNot(e.ToGomegaMatcher()) + mt.AssertExpectations(t) +} + +func TestWorksWithGomegaAssertionFailingCondition(t *testing.T) { + mt := internal.NewMockT() + g := gomega.NewGomegaWithT(mt) + e, err := NewExpectation("release-0", "namespace", + WithListClientBuilder(getMockListerBuilder(1, false)), + WithReleaseCondition(conditions.All( + jq.Equality(`.chart.metadata.name`, "chart-0"), + jq.Equality(`.chart.metadata.version`, "x.y.z"), + )), + WithReleaseListCondition(conditions.Count(1)), + WithValueCondition(conditions.All( + jq.Equality(`.foo`, "wrong"), // wrong value + jq.Equality(`.baz`, "qux"), + )), + ) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + mt.On("Helper").Return() + g.Eventually(e.AsGomegaSubject()).WithContext(ctx).ShouldNot(e.ToGomegaMatcher()) + mt.AssertExpectations(t) + + mt.On("Fatalf", mock.Anything, mock.Anything).Return() + g.Eventually(e.AsGomegaSubject()).WithContext(ctx).Should(e.ToGomegaMatcher()) + mt.AssertExpectations(t) +} + +func (ml mockLister) Run() ([]*release.Release, error) { + if ml.shoudError { + return nil, fmt.Errorf("some error") + } + releases := make([]*release.Release, ml.count) + for i := 0; i < ml.count; i++ { + releases[i] = &release.Release{ + Name: fmt.Sprintf("release-%d", i), + Config: map[string]interface{}{ + "foo": "bar", + "baz": "qux", + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: fmt.Sprintf("chart-%d", i), + Version: "x.y.z", + }, + }, + } + } + return releases, nil +} + +func getMockListerBuilder(count int, shouldError bool) func() (ListRunner, error) { + return func() (ListRunner, error) { + return mockLister{ + count: count, + shoudError: shouldError, + }, nil + } +} diff --git a/packages/testutils/expectations/kube/README.md b/packages/testutils/expectations/kube/README.md new file mode 100644 index 000000000..36777ab9c --- /dev/null +++ b/packages/testutils/expectations/kube/README.md @@ -0,0 +1,351 @@ + + +# kube + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/expectations/kube" +``` + +## Index + +- [Variables](<#variables>) +- [func NewAnnotationMatchCondition\(annotation string, value string\) types.Condition](<#NewAnnotationMatchCondition>) +- [func NewKubernetesStatusCondition\(conditionType string, status bool\) types.Condition](<#NewKubernetesStatusCondition>) +- [func NewLabelMatchCondition\(label string, value string\) types.Condition](<#NewLabelMatchCondition>) +- [func ProvisioningStatusComponentOutput\(componentKey string, value interface\{\}\) types.Condition](<#ProvisioningStatusComponentOutput>) +- [type KubeExpectation](<#KubeExpectation>) + - [func AbsentInstance\(name, namespace string, opts ...Option\) \(\*KubeExpectation, error\)](<#AbsentInstance>) + - [func AbsentPod\(name, namespace string, opts ...Option\) \(\*KubeExpectation, error\)](<#AbsentPod>) + - [func AbsentResource\(name, namespace string, gvk schema.GroupVersionKind, opts ...Option\) \(\*KubeExpectation, error\)](<#AbsentResource>) + - [func AbsentSolution\(name, namespace string, opts ...Option\) \(\*KubeExpectation, error\)](<#AbsentSolution>) + - [func AbsentTarget\(name, namespace string, opts ...Option\) \(\*KubeExpectation, error\)](<#AbsentTarget>) + - [func Instance\(name, namespace string, opts ...Option\) \(\*KubeExpectation, error\)](<#Instance>) + - [func Must\(resource \*KubeExpectation, err error\) \*KubeExpectation](<#Must>) + - [func Pod\(name, namespace string, opts ...Option\) \(\*KubeExpectation, error\)](<#Pod>) + - [func Resource\(pattern, namespace string, gvk schema.GroupVersionKind, opts ...Option\) \(\*KubeExpectation, error\)](<#Resource>) + - [func Solution\(name, namespace string, opts ...Option\) \(\*KubeExpectation, error\)](<#Solution>) + - [func Target\(name, namespace string, opts ...Option\) \(\*KubeExpectation, error\)](<#Target>) + - [func \(e \*KubeExpectation\) AsGomegaSubject\(\) func\(context.Context\) \(interface\{\}, error\)](<#KubeExpectation.AsGomegaSubject>) + - [func \(re \*KubeExpectation\) Description\(\) string](<#KubeExpectation.Description>) + - [func \(re \*KubeExpectation\) Id\(\) string](<#KubeExpectation.Id>) + - [func \(e \*KubeExpectation\) ToGomegaMatcher\(\) gomega.GomegaMatcher](<#KubeExpectation.ToGomegaMatcher>) + - [func \(re \*KubeExpectation\) Verify\(c context.Context\) error](<#KubeExpectation.Verify>) +- [type Option](<#Option>) + - [func IsAbsent\(\) Option](<#IsAbsent>) + - [func WithCondition\(condition types.Condition\) Option](<#WithCondition>) + - [func WithDescription\(description string\) Option](<#WithDescription>) + - [func WithDiscoveryClientBuilder\(builder func\(\) \(discovery.DiscoveryInterface, error\)\) Option](<#WithDiscoveryClientBuilder>) + - [func WithDynamicClientBuilder\(builder func\(\) \(dynamic.Interface, error\)\) Option](<#WithDynamicClientBuilder>) + - [func WithListCondition\(condition types.Condition\) Option](<#WithListCondition>) + - [func WithLogger\(logger func\(format string, args ...interface\{\}\)\) Option](<#WithLogger>) + - [func WithTick\(tick time.Duration\) Option](<#WithTick>) + + +## Variables + + + +```go +var ( + PodReadyCondition types.Condition = conditions.All( + NewKubernetesStatusCondition("Ready", true), + NewKubernetesStatusCondition("Initialized", true), + NewKubernetesStatusCondition("ContainersReady", true), + ) // can be used for pods and certificates + + DeploymentCompleteCondition types.Condition = conditions.All( + NewKubernetesStatusCondition("Available", true), + NewKubernetesStatusCondition("Progressing", true), + ) // can be used for deployments and statefulsets + + // AioManagerLabelCondition is a condition that checks if the resource is managed by the aio orc api + AioManagerLabelCondition types.Condition = NewLabelMatchCondition("iotoperations.azure.com/managed-by", "symphony-api") + + // ProvisioningSucceededCondition is a condition that checks if the resource has succeeded provisioning + ProvisioningSucceededCondition types.Condition = jq.Equality(".status.provisioningStatus.status", "Succeeded", statusDescription) + + // ProvisioningFailedCondition is a condition that checks if the resource has failed provisioning + ProvisioningFailedCondition types.Condition = jq.Equality(".status.provisioningStatus.status", "Failed", statusDescription) + // OperationIdMatchCondition is a condition that checks if the resource has the operation id annotation and + // ensures that it matches the operationId in the status of the resource + OperationIdMatchCondition types.Condition = jq.MustNew( + fmt.Sprintf(`.metadata.annotations["%s"]`, "management.azure.com/operationId"), + jq.WithCustomMatcher(operationJqMatcher), + jq.WithDescription("Operation Id"), + ) +) +``` + + +## func [NewAnnotationMatchCondition]() + +```go +func NewAnnotationMatchCondition(annotation string, value string) types.Condition +``` + +NewAnnotationMatchCondition returns a condition that checks if the resource has the annotation and value + + +## func [NewKubernetesStatusCondition]() + +```go +func NewKubernetesStatusCondition(conditionType string, status bool) types.Condition +``` + +NewKubernetesStatusCondition returns a condition that checks the status of a kubernetes resource's condition + + +## func [NewLabelMatchCondition]() + +```go +func NewLabelMatchCondition(label string, value string) types.Condition +``` + +NewLabelMatchCondition returns a condition that checks if the resource has the label and value + + +## func [ProvisioningStatusComponentOutput]() + +```go +func ProvisioningStatusComponentOutput(componentKey string, value interface{}) types.Condition +``` + + + + +## type [KubeExpectation]() + + + +```go +type KubeExpectation struct { + // contains filtered or unexported fields +} +``` + + +### func [AbsentInstance]() + +```go +func AbsentInstance(name, namespace string, opts ...Option) (*KubeExpectation, error) +``` + +AbsentInstance returns an expectation that the instance\(s\) is/are absent from the cluster + + +### func [AbsentPod]() + +```go +func AbsentPod(name, namespace string, opts ...Option) (*KubeExpectation, error) +``` + +AbsentPod returns an expectation that the pod\(s\) is/are absent from the cluster + + +### func [AbsentResource]() + +```go +func AbsentResource(name, namespace string, gvk schema.GroupVersionKind, opts ...Option) (*KubeExpectation, error) +``` + +AbsentResource returns an expectation for the resources is/are absent from the cluster + + +### func [AbsentSolution]() + +```go +func AbsentSolution(name, namespace string, opts ...Option) (*KubeExpectation, error) +``` + +AbsentSolution returns an expectation that the solution\(s\) is/are absent from the cluster + + +### func [AbsentTarget]() + +```go +func AbsentTarget(name, namespace string, opts ...Option) (*KubeExpectation, error) +``` + +AbsentTarget returns an expectation that the target\(s\) is/are absent from the cluster + + +### func [Instance]() + +```go +func Instance(name, namespace string, opts ...Option) (*KubeExpectation, error) +``` + +Instance returns an expectation for a instance\(s\) in the cluster + + +### func [Must]() + +```go +func Must(resource *KubeExpectation, err error) *KubeExpectation +``` + +Must returns a resource expectation or panics if there is an error + + +### func [Pod]() + +```go +func Pod(name, namespace string, opts ...Option) (*KubeExpectation, error) +``` + +Pod returns an expectation expectation for a pod\(s\) in the cluster + + +### func [Resource]() + +```go +func Resource(pattern, namespace string, gvk schema.GroupVersionKind, opts ...Option) (*KubeExpectation, error) +``` + + + + +### func [Solution]() + +```go +func Solution(name, namespace string, opts ...Option) (*KubeExpectation, error) +``` + +Solution returns an expectation for a solution\(s\) in the cluster + + +### func [Target]() + +```go +func Target(name, namespace string, opts ...Option) (*KubeExpectation, error) +``` + +Target returns an expectation for a target\(s\) in the cluster + + +### func \(\*KubeExpectation\) [AsGomegaSubject]() + +```go +func (e *KubeExpectation) AsGomegaSubject() func(context.Context) (interface{}, error) +``` + + + + +### func \(\*KubeExpectation\) [Description]() + +```go +func (re *KubeExpectation) Description() string +``` + + + + +### func \(\*KubeExpectation\) [Id]() + +```go +func (re *KubeExpectation) Id() string +``` + + + + +### func \(\*KubeExpectation\) [ToGomegaMatcher]() + +```go +func (e *KubeExpectation) ToGomegaMatcher() gomega.GomegaMatcher +``` + + + + +### func \(\*KubeExpectation\) [Verify]() + +```go +func (re *KubeExpectation) Verify(c context.Context) error +``` + +Verify implements types.Expectation. + + +## type [Option]() + + + +```go +type Option func(*KubeExpectation) +``` + + +### func [IsAbsent]() + +```go +func IsAbsent() Option +``` + + + + +### func [WithCondition]() + +```go +func WithCondition(condition types.Condition) Option +``` + +WithCondition specifies the conditions that the resource should satisfy. + + +### func [WithDescription]() + +```go +func WithDescription(description string) Option +``` + + + + +### func [WithDiscoveryClientBuilder]() + +```go +func WithDiscoveryClientBuilder(builder func() (discovery.DiscoveryInterface, error)) Option +``` + + + + +### func [WithDynamicClientBuilder]() + +```go +func WithDynamicClientBuilder(builder func() (dynamic.Interface, error)) Option +``` + + + + +### func [WithListCondition]() + +```go +func WithListCondition(condition types.Condition) Option +``` + +WithListCondition specifies the conditions that the list of matched resources should satisfy. + + +### func [WithLogger]() + +```go +func WithLogger(logger func(format string, args ...interface{})) Option +``` + +WithLogger specifies the logger to be used. + + +### func [WithTick]() + +```go +func WithTick(tick time.Duration) Option +``` + +WithTick specifies the tick for the expectation. + +Generated by [gomarkdoc]() diff --git a/packages/testutils/expectations/kube/commons.go b/packages/testutils/expectations/kube/commons.go new file mode 100644 index 000000000..ae40c0393 --- /dev/null +++ b/packages/testutils/expectations/kube/commons.go @@ -0,0 +1,177 @@ +package kube + +import ( + "context" + "fmt" + + "github.com/eclipse-symphony/symphony/packages/testutils/conditions" + "github.com/eclipse-symphony/symphony/packages/testutils/conditions/jq" + "github.com/eclipse-symphony/symphony/packages/testutils/conditions/jsonpath" + "github.com/eclipse-symphony/symphony/packages/testutils/helpers" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + statusDescription = jq.WithDescription("Provisioning Status") +) + +var ( + /** + * These are some common kubernetes resource conditions + */ + + PodReadyCondition types.Condition = conditions.All( + NewKubernetesStatusCondition("Ready", true), + NewKubernetesStatusCondition("Initialized", true), + NewKubernetesStatusCondition("ContainersReady", true), + ) // can be used for pods and certificates + + DeploymentCompleteCondition types.Condition = conditions.All( + NewKubernetesStatusCondition("Available", true), + NewKubernetesStatusCondition("Progressing", true), + ) // can be used for deployments and statefulsets + + /** + * These are some common conditions for azure iot orchestration resources + */ + + // AioManagerLabelCondition is a condition that checks if the resource is managed by the aio orc api + AioManagerLabelCondition types.Condition = NewLabelMatchCondition("iotoperations.azure.com/managed-by", "symphony-api") + + // ProvisioningSucceededCondition is a condition that checks if the resource has succeeded provisioning + ProvisioningSucceededCondition types.Condition = jq.Equality(".status.provisioningStatus.status", "Succeeded", statusDescription) + + // ProvisioningFailedCondition is a condition that checks if the resource has failed provisioning + ProvisioningFailedCondition types.Condition = jq.Equality(".status.provisioningStatus.status", "Failed", statusDescription) + // OperationIdMatchCondition is a condition that checks if the resource has the operation id annotation and + // ensures that it matches the operationId in the status of the resource + OperationIdMatchCondition types.Condition = jq.MustNew( + fmt.Sprintf(`.metadata.annotations["%s"]`, "management.azure.com/operationId"), + jq.WithCustomMatcher(operationJqMatcher), + jq.WithDescription("Operation Id"), + ) +) + +func operationJqMatcher(ctx context.Context, value, resource interface{}, log logger.Logger) error { + operationId, err := getProvisioningOperationIdFromStatus(resource.(map[string]interface{})) + if err != nil { + return err + } + switch value := value.(type) { + case string: + log("Comparing %s with %s", operationId, value) + if operationId == value { + return nil + } + return fmt.Errorf("expected %s, got %s", operationId, value) + default: + return fmt.Errorf("expected operationId to be string, got %T", value) + } +} + +func getProvisioningOperationIdFromStatus(resource map[string]interface{}) (string, error) { + status, ok := resource["status"].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("status field not found") + } + provisioningStatus, ok := status["provisioningStatus"].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("provisioningStatus field not found") + } + operationId, ok := provisioningStatus["operationId"].(string) + if !ok { + return "", fmt.Errorf("operationId field not found") + } + return operationId, nil +} + +// NewKubernetesStatusCondition returns a condition that checks the status of a kubernetes resource's condition +func NewKubernetesStatusCondition(conditionType string, status bool) types.Condition { + statusString := "False" + if status { + statusString = "True" + } + return jsonpath.MustNew( + fmt.Sprintf("$.status.conditions[?(@.type == '%s')].status", conditionType), + jsonpath.WithValue([]interface{}{statusString}), + jsonpath.WithDescription(fmt.Sprintf("Condition %s", conditionType)), + ) +} + +// NewLabelMatchCondition returns a condition that checks if the resource has the label and value +func NewLabelMatchCondition(label string, value string) types.Condition { + return jq.Equality( + fmt.Sprintf(`.metadata.labels["%s"]`, label), + jq.WithValue(value), + jq.WithDescription(fmt.Sprintf("Label %s", label)), + ) +} + +func ProvisioningStatusComponentOutput(componentKey string, value interface{}) types.Condition { + return jq.Equality(fmt.Sprintf(`.status.provisioningStatus.output["%s"]`, componentKey), value) +} + +// AbsentResource returns an expectation for the resources is/are absent from the cluster +func AbsentResource(name, namespace string, gvk schema.GroupVersionKind, opts ...Option) (*KubeExpectation, error) { + opts = append(opts, IsAbsent()) + return Resource(name, namespace, gvk, opts...) +} + +// NewAnnotationMatchCondition returns a condition that checks if the resource has the annotation and value +func NewAnnotationMatchCondition(annotation string, value string) types.Condition { + return jq.MustNew( + fmt.Sprintf(`.metadata.annotations["%s"]`, annotation), + jq.WithValue(value), + jq.WithDescription(fmt.Sprintf("Annotation %s", annotation)), + ) +} + +// Pod returns an expectation expectation for a pod(s) in the cluster +func Pod(name, namespace string, opts ...Option) (*KubeExpectation, error) { + return Resource(name, namespace, helpers.PodGVK, opts...) +} + +// AbsentPod returns an expectation that the pod(s) is/are absent from the cluster +func AbsentPod(name, namespace string, opts ...Option) (*KubeExpectation, error) { + return AbsentResource(name, namespace, helpers.PodGVK, opts...) +} + +// Target returns an expectation for a target(s) in the cluster +func Target(name, namespace string, opts ...Option) (*KubeExpectation, error) { + return Resource(name, namespace, helpers.TargetGVK, opts...) +} + +// AbsentTarget returns an expectation that the target(s) is/are absent from the cluster +func AbsentTarget(name, namespace string, opts ...Option) (*KubeExpectation, error) { + return AbsentResource(name, namespace, helpers.TargetGVK, opts...) +} + +// Instance returns an expectation for a instance(s) in the cluster +func Instance(name, namespace string, opts ...Option) (*KubeExpectation, error) { + return Resource(name, namespace, helpers.InstanceGVK, opts...) +} + +// AbsentInstance returns an expectation that the instance(s) is/are absent from the cluster +func AbsentInstance(name, namespace string, opts ...Option) (*KubeExpectation, error) { + return AbsentResource(name, namespace, helpers.InstanceGVK, opts...) +} + +// Solution returns an expectation for a solution(s) in the cluster +func Solution(name, namespace string, opts ...Option) (*KubeExpectation, error) { + return Resource(name, namespace, helpers.SolutionGVK, opts...) +} + +// AbsentSolution returns an expectation that the solution(s) is/are absent from the cluster +func AbsentSolution(name, namespace string, opts ...Option) (*KubeExpectation, error) { + return AbsentResource(name, namespace, helpers.SolutionGVK, opts...) +} + +// Must returns a resource expectation or panics if there is an error +func Must(resource *KubeExpectation, err error) *KubeExpectation { + if err != nil { + panic(err) + } + return resource +} diff --git a/packages/testutils/expectations/kube/gomega.go b/packages/testutils/expectations/kube/gomega.go new file mode 100644 index 000000000..96de220b9 --- /dev/null +++ b/packages/testutils/expectations/kube/gomega.go @@ -0,0 +1,34 @@ +package kube + +import ( + "context" + "fmt" + + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/onsi/gomega/gcustom" + gomega "github.com/onsi/gomega/types" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var ( + _ types.GomegaEventuallySubject = &KubeExpectation{} +) + +func (e *KubeExpectation) AsGomegaSubject() func(context.Context) (interface{}, error) { + return func(c context.Context) (interface{}, error) { + return e.getResults(c) + } +} + +func (e *KubeExpectation) ToGomegaMatcher() gomega.GomegaMatcher { + return gcustom.MakeMatcher(func(resource interface{}) (bool, error) { + list, ok := resource.([]*unstructured.Unstructured) + if !ok { + return false, fmt.Errorf("expected resource to be a list of unstructured.Unstructured, got %T", resource) + } + if err := e.verifyConditions(context.TODO(), list); err != nil { + return false, nil + } + return true, nil + }) +} diff --git a/packages/testutils/expectations/kube/options.go b/packages/testutils/expectations/kube/options.go new file mode 100644 index 000000000..5448a2846 --- /dev/null +++ b/packages/testutils/expectations/kube/options.go @@ -0,0 +1,61 @@ +package kube + +import ( + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" +) + +// WithCondition specifies the conditions that the resource should satisfy. +func WithCondition(condition types.Condition) Option { + return func(re *KubeExpectation) { + re.condition = condition + } +} + +// WithListCondition specifies the conditions that the list of matched resources should satisfy. +func WithListCondition(condition types.Condition) Option { + return func(re *KubeExpectation) { + re.listCondition = condition + } +} + +// WithLogger specifies the logger to be used. +func WithLogger(logger func(format string, args ...interface{})) Option { + return func(re *KubeExpectation) { + re.l = logger + } +} + +// WithTick specifies the tick for the expectation. +func WithTick(tick time.Duration) Option { + return func(re *KubeExpectation) { + re.tick = tick + } +} + +func WithDescription(description string) Option { + return func(h *KubeExpectation) { + h.description = description + } +} + +func WithDynamicClientBuilder(builder func() (dynamic.Interface, error)) Option { + return func(h *KubeExpectation) { + h.dynamicClientBuilder = builder + } +} + +func WithDiscoveryClientBuilder(builder func() (discovery.DiscoveryInterface, error)) Option { + return func(h *KubeExpectation) { + h.discoveryClientBuilder = builder + } +} + +func IsAbsent() Option { + return func(re *KubeExpectation) { + re.removed = true + } +} diff --git a/packages/testutils/expectations/kube/resource.go b/packages/testutils/expectations/kube/resource.go new file mode 100644 index 000000000..f8b61adad --- /dev/null +++ b/packages/testutils/expectations/kube/resource.go @@ -0,0 +1,284 @@ +package kube + +import ( + "context" + "fmt" + "regexp" + "strings" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/conditions" + "github.com/eclipse-symphony/symphony/packages/testutils/helpers" + ectx "github.com/eclipse-symphony/symphony/packages/testutils/internal/context" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/google/uuid" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/discovery/cached/memory" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/restmapper" +) + +type ( + KubeExpectation struct { + // pattern is the prefix of the resource pattern + pattern string + + // description is a friendly description of the expectation + description string + + // gvk is the group, version, kind of the resource + gvk schema.GroupVersionKind + + // namespace is the namespace of the resource if applicable + namespace string + + // Removed indicates whether the resource is expected to be present or not + removed bool + + // conditions specifies the condition that the resource should satisfy + condition types.Condition + + // listCondition specifies the condition that the list of resources should satisfy + listCondition types.Condition + + discoveryClient discovery.DiscoveryInterface + dynamicClient dynamic.Interface + mapper meta.RESTMapper + discoveryClientBuilder func() (discovery.DiscoveryInterface, error) + dynamicClientBuilder func() (dynamic.Interface, error) + + tick time.Duration + l func(format string, args ...interface{}) + nameRegex *regexp.Regexp + level int + id string + initialized bool + } + + Option func(*KubeExpectation) +) + +const ( + defaultTimeout = 10 * time.Minute + defaultTick = 10 * time.Second +) + +var ( + _ types.Expectation = &KubeExpectation{} +) + +func Resource(pattern, namespace string, gvk schema.GroupVersionKind, opts ...Option) (*KubeExpectation, error) { + re := KubeExpectation{ + pattern: boundPattern(pattern), + gvk: gvk, + tick: defaultTick, + discoveryClientBuilder: defaultDiscoveryClientBuilder, + dynamicClientBuilder: defaultDynamicClientBuilder, + namespace: namespace, + id: uuid.NewString(), + } + + compiled, err := regexp.Compile(boundPattern(pattern)) + if err != nil { + return nil, err + } + re.nameRegex = compiled + + for _, opt := range opts { + opt(&re) + } + + re.initializeCountCondition() + return &re, nil +} + +func (re *KubeExpectation) initClients() error { + if re.initialized { + return nil + } + discoveryClient, err := re.discoveryClientBuilder() + if err != nil { + return err + } + re.discoveryClient = discoveryClient + + dynamicClient, err := re.dynamicClientBuilder() + if err != nil { + return err + } + re.dynamicClient = dynamicClient + re.mapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(re.discoveryClient)) + re.initialized = true + return nil +} + +func (re *KubeExpectation) initializeCountCondition() { + countCondition := conditions.GreaterThan(0) + if re.removed { + countCondition = conditions.Count(0) + } + if re.listCondition != nil { + re.listCondition = conditions.All(countCondition, re.listCondition) + } else { + re.listCondition = countCondition + } +} + +func (re *KubeExpectation) log(format string, args ...interface{}) { + s := fmt.Sprintf(format, args...) + format = "%s[%s]: %s\n" + args = []interface{}{strings.Repeat(" ", re.level), re.Description(), s} + + if re.l != nil { + re.l(format, args...) + } else { + logger.GetDefaultLogger()(format, args...) + } +} + +// Verify implements types.Expectation. +func (re *KubeExpectation) Verify(c context.Context) error { + ctx := ectx.From(c) + re.level = ctx.Level() + + return helpers.Eventually(ctx, func(ctx context.Context) error { + re.log(strings.Repeat("-", 80)) + re.log(`Verifying resource`) + err := re.verify(ctx, re.condition) + if err != nil { + re.log("Resource verification failed: %v", err) + return err + } + return nil + }, re.tick, "Timed out while verifying resource %s of kind: [%s]", re.pattern, re.gvk.String()) +} + +func (re *KubeExpectation) Description() string { + if re.description != "" { + return re.description + } + return fmt.Sprintf("%s: %s", re.gvk.String(), re.pattern) +} + +func (re *KubeExpectation) Id() string { + return re.id +} + +func (re *KubeExpectation) getResults(ctx context.Context) ([]*unstructured.Unstructured, error) { + if err := re.initClients(); err != nil { + return nil, err + } + var namespaced bool + + mapping, err := re.mapper.RESTMapping(re.gvk.GroupKind(), re.gvk.Version) + if err != nil { + return nil, err + } + namespaced = mapping.Scope.Name() == meta.RESTScopeNameNamespace + + if namespaced && re.namespace == "" { + return nil, fmt.Errorf("namespace is required for namespaced resources") + } + + var list *unstructured.UnstructuredList + + if namespaced { + namespace := re.namespace + if namespace == "*" { + namespace = metav1.NamespaceAll + } + list, err = re.dynamicClient.Resource(mapping.Resource).Namespace(namespace).List(ctx, metav1.ListOptions{}) + } else { + list, err = re.dynamicClient.Resource(mapping.Resource).List(ctx, metav1.ListOptions{}) + } + + if err != nil { + return nil, err + } + + return re.getMatches(list), nil +} + +func (re *KubeExpectation) verify(ctx context.Context, condition types.Condition) (err error) { + + matches, err := re.getResults(ctx) + if err != nil { + return err + } + re.log("Resource matches returned. %d matches", len(matches)) + + return re.verifyConditions(ctx, matches) +} + +func (re *KubeExpectation) verifyConditions(ctx context.Context, matches []*unstructured.Unstructured) (err error) { + err = re.evaluateListCondition(ctx, matches, re.listCondition) + if err != nil { + return + } + + err = re.evaluateCondition(ctx, matches, re.condition) + if err != nil { + return + } + + return nil +} + +func (re *KubeExpectation) evaluateCondition(c context.Context, objects []*unstructured.Unstructured, condition types.Condition) (err error) { + ctx := ectx.From(c) + if condition != nil { + for _, object := range objects { + err = condition.IsSatisfiedBy(ctx.Nested(), object.Object) + if err != nil { + return err + } + } + } + + return nil +} + +func (re *KubeExpectation) evaluateListCondition(c context.Context, objects []*unstructured.Unstructured, condition types.Condition) (err error) { + ctx := ectx.From(c) + if condition != nil { + err = condition.IsSatisfiedBy(ctx.Nested(), objects) + if err != nil { + return err + } + } + + return nil +} + +func (re *KubeExpectation) getMatches(list *unstructured.UnstructuredList) []*unstructured.Unstructured { + matches := make([]*unstructured.Unstructured, 0) + for i := range list.Items { + if re.nameRegex.MatchString(list.Items[i].GetName()) { + matches = append(matches, &list.Items[i]) + } + } + return matches +} + +func boundPattern(str string) string { + if !strings.HasPrefix(str, "^") { + str = "^" + str + } + if !strings.HasSuffix(str, "$") { + str = str + "$" + } + return str +} + +func defaultDiscoveryClientBuilder() (discovery.DiscoveryInterface, error) { + return helpers.DiscoveryClient() +} + +func defaultDynamicClientBuilder() (dynamic.Interface, error) { + return helpers.DynamicClient() +} diff --git a/packages/testutils/expectations/kube/resource_test.go b/packages/testutils/expectations/kube/resource_test.go new file mode 100644 index 000000000..86eebc749 --- /dev/null +++ b/packages/testutils/expectations/kube/resource_test.go @@ -0,0 +1,419 @@ +package kube + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/conditions" + "github.com/eclipse-symphony/symphony/packages/testutils/conditions/jq" + "github.com/eclipse-symphony/symphony/packages/testutils/helpers" + "github.com/eclipse-symphony/symphony/packages/testutils/internal" + gomega "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + fakeDiscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/dynamic" + fakeDynamic "k8s.io/client-go/dynamic/fake" +) + +var ( + testResources = []runtime.Object{ + internal.Pod("test-1", "namespace-1"), + internal.Pod("test-2", "namespace-2"), + internal.Pod("different-1", "namespace-1"), + internal.Pod("different-2", "namespace-2"), + internal.Resource("config-1", "namespace-1", helpers.GVK("", "v1", "ConfigMap")), + internal.Resource("config-2", "namespace-2", helpers.GVK("", "v1", "ConfigMap")), + internal.Target("test-1", "namespace-1"), + internal.OutOfSyncResource("test-2", "namespace-2", helpers.TargetGVK), + internal.Namespace("namespace-1"), + internal.Namespace("namespace-2"), + } + testScheme = getScheme() + testDynamicClient = fakeDynamic.NewSimpleDynamicClient(testScheme, testResources...) + testDiscovery = generateTestDiscoveryClient() + testTimeout = time.Millisecond * 5 +) + +func getScheme() *runtime.Scheme { + s := runtime.NewScheme() + s.AddKnownTypes( + schema.GroupVersion{Group: "", Version: "v1"}, + &corev1.Pod{}, + &corev1.PodList{}, + &corev1.Namespace{}, + &corev1.ConfigMap{}, + &corev1.ConfigMapList{}, + &corev1.NamespaceList{}, + ) + s.AddKnownTypes( + helpers.TargetGVK.GroupVersion(), + &unstructured.Unstructured{}, + &unstructured.UnstructuredList{}, + ) + return s +} + +func generateTestDiscoveryClient() *fakeDiscovery.FakeDiscovery { + f := &fakeDiscovery.FakeDiscovery{ + Fake: &testDynamicClient.Fake, + } + f.Resources = internal.GenerateTestApiResourceList() + return f +} + +func testDynamicClientBuilder() (dynamic.Interface, error) { + return testDynamicClient, nil +} + +func testDiscoveryClientBuilder() (discovery.DiscoveryInterface, error) { + return testDiscovery, nil +} + +func TestSuccess(t *testing.T) { + r, err := Resource("test", "*", helpers.GVK("", "v1", "Pod")) + require.NoError(t, err) + require.NotEmpty(t, r.Description()) + require.NotEmpty(t, r.Id()) +} + +func TestSuccessAlternateDescription(t *testing.T) { + r, err := Resource("test", "*", helpers.GVK("", "v1", "Pod"), WithDescription("alternate")) + require.NoError(t, err) + require.Equal(t, "alternate", r.Description()) +} + +func TestFailOnInvalidPattern(t *testing.T) { + _, err := Resource("test(", "namespace-1", helpers.GVK("", "v1", "Pod")) // invalid pattern + require.Error(t, err) +} + +func TestMustSucceed(t *testing.T) { + require.NotPanics(t, func() { + Must(Resource("test", "*", helpers.GVK("", "v1", "Pod"))) + }) +} + +func TestMustPanics(t *testing.T) { + require.Panics(t, func() { + Must(Resource("test (", "", helpers.GVK("", "v1", "Pod"))) // invalid pattern + }) +} + +func TestFindsSinglePodInNamespace(t *testing.T) { + e := Must(Resource("test-1", "namespace-1", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithCondition( + jq.MustNew(".spec.containers[0].name", jq.WithValue("test-1")), + ), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestFindsSinglePodInNamespaceWithFailingCondition(t *testing.T) { + e := Must(Resource("test-1", "namespace-1", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithCondition( + jq.MustNew(".spec.containers[0].name", jq.WithValue("wrong")), + ), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestExpectedCountSuccess(t *testing.T) { + e := Must(Resource("test.+", "*", helpers.GVK("", "v1", "Pod"), // finds all pods starting with "test" in all namespaces + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithListCondition(conditions.Count(2)), // correct number + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestClusterLevelResourceSuccess(t *testing.T) { + e := Must(Resource("namespace-.*", "*", helpers.GVK("", "v1", "Namespace"), // finds a namespace at the cluster level + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithListCondition(conditions.Count(2)), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestExpectedCountFail(t *testing.T) { + e := Must(Resource("test.+", "*", helpers.GVK("", "v1", "Pod"), // finds all pods starting with "test" in all namespaces + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithListCondition(conditions.Count(0)), // wrong number + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestAbsentPodSuccess(t *testing.T) { + e := Must(AbsentPod("nonexistent", "*", // tries to find a pod that doesn't exist + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestAbsentPodFail(t *testing.T) { + e := Must(AbsentPod("test-1", "namespace-1", // tries to find a pod that shouldn't exist but it does + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestAbsentResourceSuccess(t *testing.T) { + expect, err := AbsentResource("nonexistent", "*", helpers.TargetGVK, + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder)) + + e := Must(expect, err) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + err = e.Verify(ctx) + require.NoError(t, err) +} + +func TestAbsentResourceFail(t *testing.T) { + e := Must(AbsentResource("test-1", "namespace-1", helpers.TargetGVK, // tries to find a target that shouldn't exist but it does + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestAbsentResourcePassWithRedundantConditions(t *testing.T) { + e := Must(AbsentResource("non-existent", "namespace-1", helpers.TargetGVK, // resource doesn't exist + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithListCondition(conditions.Count(0)), // redundant condition + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestUnknownResourceFail(t *testing.T) { + e := Must(Resource("test-1", "namespace-1", helpers.GVK("random.group", "v1", "unknown"), // tries to find a Resource type that is not known + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestFailOnNoNamespaceForNamespacedResource(t *testing.T) { + e := Must(Pod("test-1", "", // tries to find a Resource type that is not known + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestShouldFailWhenDiscoveryFails(t *testing.T) { + ex, err := Resource("test-1", "namespace-1", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(func() (discovery.DiscoveryInterface, error) { + return nil, errors.New("discovery failed") + }), + ) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + require.Error(t, ex.Verify(ctx)) +} + +func TestShouldFailWhenDynamicFails(t *testing.T) { + ex, err := Resource("test-1", "namespace-1", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(func() (dynamic.Interface, error) { + return nil, errors.New("dynamic failed") + }), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + ) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + require.Error(t, ex.Verify(ctx)) +} + +func TestCanUseCustomLogger(t *testing.T) { + called := false + logger := func(format string, args ...interface{}) { + called = true + } + e := Must(Resource("test-1", "namespace-1", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithLogger(logger), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) + require.True(t, called) +} + +func TestCanUseCustomTickInterval(t *testing.T) { + e := Must(Resource("test-1", "namespace-1", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithTick(time.Millisecond), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) + require.Equal(t, time.Millisecond, e.tick) +} + +func TestAnnotationMathSuccesfulOnResource(t *testing.T) { + e := Must(Resource("test-1", "namespace-1", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithCondition( + NewAnnotationMatchCondition("test-annotation", "test-annotation-value"), + ), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} +func TestCombinedSuccessfullConditions(t *testing.T) { + e := Must(Resource("test-.*", "*", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithCondition(conditions.All( + NewAnnotationMatchCondition("test-annotation", "test-annotation-value"), + NewAnnotationMatchCondition("management.azure.com/operationId", "test-operation-id"), + )), + WithListCondition(conditions.Count(2)), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestOperationIdTargetSuccess(t *testing.T) { + e := Must(Resource("test-1", "namespace-1", helpers.GVK("fabric.symphony", "v1", "Target"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithCondition(OperationIdMatchCondition), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.NoError(t, e.Verify(ctx)) +} + +func TestOperationIdTargetFail(t *testing.T) { + e := Must(Resource("test-2", "namespace-2", helpers.GVK("fabric.symphony", "v1", "Target"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithCondition(OperationIdMatchCondition), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + require.Error(t, e.Verify(ctx)) +} + +func TestCommonCommonConstructors(t *testing.T) { + _, err := Target("test-1", "namespace-1") + require.NoError(t, err) + + _, err = AbsentTarget("test-1", "namespace-1") + require.NoError(t, err) + + _, err = Instance("test-1", "namespace-1") + require.NoError(t, err) + + _, err = AbsentInstance("test-1", "namespace-1") + require.NoError(t, err) + + _, err = Solution("test-1", "namespace-1") + require.NoError(t, err) + + _, err = AbsentSolution("test-1", "namespace-1") + require.NoError(t, err) +} + +func TestShouldWorkWithGomegaAssersions(t *testing.T) { + mt := internal.NewMockT() + g := gomega.NewWithT(mt) + e := Must(Resource("test-.*", "*", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithCondition(conditions.All( + NewAnnotationMatchCondition("test-annotation", "test-annotation-value"), + NewAnnotationMatchCondition("management.azure.com/operationId", "test-operation-id"), + )), + WithListCondition(conditions.Count(2)), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + mt.On("Helper").Return() + g.Eventually(e.AsGomegaSubject()).WithContext(ctx).Should(e.ToGomegaMatcher()) + mt.AssertExpectations(t) + + ctx, cancel = context.WithTimeout(context.Background(), testTimeout) + defer cancel() + mt.On("Fatalf", mock.Anything, mock.Anything).Return() + g.Eventually(e.AsGomegaSubject()).WithContext(ctx).ShouldNot(e.ToGomegaMatcher()) + mt.AssertExpectations(t) +} + +func TestShouldWorkWithInvertedGomegaAssersions(t *testing.T) { + mt := internal.NewMockT() + g := gomega.NewWithT(mt) + e := Must(Resource("test-.*", "*", helpers.GVK("", "v1", "Pod"), + WithDynamicClientBuilder(testDynamicClientBuilder), + WithDiscoveryClientBuilder(testDiscoveryClientBuilder), + WithCondition(conditions.All( + NewAnnotationMatchCondition("test-annotation", "test-annotation-value"), + NewAnnotationMatchCondition("management.azure.com/operationId", "test-operation-id"), + jq.Equality(".spec.containers[0].name", "wrong"), + )), + WithListCondition(conditions.Count(2)), + )) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + mt.On("Helper").Return() + failingCall := mt.On("Fatalf", mock.Anything, mock.Anything).Return() + g.Eventually(e.AsGomegaSubject()).WithContext(ctx).Should(e.ToGomegaMatcher()) + mt.AssertExpectations(t) + + failingCall.Unset() + g.Eventually(e.AsGomegaSubject()).WithContext(ctx).ShouldNot(e.ToGomegaMatcher()) + mt.AssertExpectations(t) +} diff --git a/packages/testutils/go.mod b/packages/testutils/go.mod new file mode 100644 index 000000000..20556f9ef --- /dev/null +++ b/packages/testutils/go.mod @@ -0,0 +1,150 @@ +module github.com/eclipse-symphony/symphony/packages/testutils + +go 1.19 + +replace github.com/eclipse-symphony/symphony/packages/mage => ../mage + +require ( + github.com/eclipse-symphony/symphony/packages/mage v0.0.0-00010101000000-000000000000 + github.com/google/uuid v1.4.0 + github.com/itchyny/gojq v0.12.13 + github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 + github.com/onsi/gomega v1.30.0 + github.com/stretchr/testify v1.8.4 + helm.sh/helm/v3 v3.10.0 + k8s.io/api v0.25.2 + k8s.io/apimachinery v0.25.2 + k8s.io/client-go v0.25.0 + +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/BurntSushi/toml v1.1.0 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.2 // indirect + github.com/Masterminds/squirrel v1.5.3 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/VividCortex/ewma v1.1.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/cheggaaa/pb/v3 v3.0.4 // indirect + github.com/containerd/containerd v1.6.6 // indirect + github.com/cyphar/filepath-securejoin v0.2.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v20.10.17+incompatible // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v20.10.17+incompatible // indirect + github.com/docker/docker-credential-helpers v0.6.4 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/go-errors/errors v1.0.1 // indirect + github.com/go-gorp/gorp/v3 v3.0.2 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/swag v0.19.14 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lib/pq v1.10.6 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/magefile/mage v1.15.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/princjef/mageutil v1.0.0 // indirect + github.com/prometheus/client_golang v1.12.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rubenv/sql-migrate v1.1.2 // indirect + github.com/russross/blackfriday v1.5.2 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/cobra v1.5.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xlab/treeprint v1.1.0 // indirect + go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect + google.golang.org/grpc v1.47.0 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.25.0 // indirect + k8s.io/apiserver v0.25.0 // indirect + k8s.io/cli-runtime v0.25.0 // indirect + k8s.io/component-base v0.25.0 // indirect + k8s.io/klog/v2 v2.70.1 // indirect + k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect + k8s.io/kubectl v0.25.0 // indirect + k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect + oras.land/oras-go v1.2.0 // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/kustomize/api v0.12.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/packages/testutils/go.sum b/packages/testutils/go.sum new file mode 100644 index 000000000..70678a01e --- /dev/null +++ b/packages/testutils/go.sum @@ -0,0 +1,1060 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= +github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= +github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NECxo= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cheggaaa/pb v2.0.7+incompatible/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/cheggaaa/pb/v3 v3.0.4 h1:QZEPYOj2ix6d5oEg63fbHmpolrnNiwjUsk+h74Yt4bM= +github.com/cheggaaa/pb/v3 v3.0.4/go.mod h1:7rgWxLrAUcFMkvJuv09+DYi7mMUYi8nO9iOWcvGJPfw= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= +github.com/containerd/containerd v1.6.6 h1:xJNPhbrmz8xAMDNoVjHy9YHtWwEQNS+CDkcIRh7t8Y0= +github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk= +github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= +github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= +github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4= +github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= +github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= +github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= +github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= +github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= +github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= +github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +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/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/matryer/is v1.3.0 h1:9qiso3jaJrOe6qBRJRBt2Ldht05qDiFP9le0JOIhRSI= +github.com/matryer/is v1.3.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +github.com/princjef/mageutil v1.0.0 h1:1OfZcJUMsooPqieOz2ooLjI+uHUo618pdaJsbCXcFjQ= +github.com/princjef/mageutil v1.0.0/go.mod h1:mkShhaUomCYfAoVvTKRcbAs8YSVPdtezI5j6K+VXhrs= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36tZeQ= +github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= +github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +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/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/VividCortex/ewma.v1 v1.1.1/go.mod h1:TekXuFipeiHWiAlO1+wSS23vTcyFau5u3rxXUSXj710= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/cheggaaa/pb.v2 v2.0.7/go.mod h1:0CiZ1p8pvtxBlQpLXkHuUTpdJ1shm3OqCF1QugkjHL4= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fatih/color.v1 v1.7.0/go.mod h1:P7yosIhqIl/sX8J8UypY5M+dDpD2KmyfP5IRs5v/fo0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mattn/go-colorable.v0 v0.1.0/go.mod h1:BVJlBXzARQxdi3nZo6f6bnl5yR20/tOL6p+V0KejgSY= +gopkg.in/mattn/go-isatty.v0 v0.0.4/go.mod h1:wt691ab7g0X4ilKZNmMII3egK0bTxl37fEn/Fwbd8gc= +gopkg.in/mattn/go-runewidth.v0 v0.0.4/go.mod h1:BmXejnxvhwdaATwiJbB1vZ2dtXkQKZGu9yLFCZb4msQ= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +helm.sh/helm/v3 v3.10.0 h1:y/MYONZ/bsld9kHwqgBX2uPggnUr5hahpjwt9/jrHlI= +helm.sh/helm/v3 v3.10.0/go.mod h1:paPw0hO5KVfrCMbi1M8+P8xdfBri3IiJiVKATZsFR94= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.25.2 h1:v6G8RyFcwf0HR5jQGIAYlvtRNrxMJQG1xJzaSeVnIS8= +k8s.io/api v0.25.2/go.mod h1:qP1Rn4sCVFwx/xIhe+we2cwBLTXNcheRyYXwajonhy0= +k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= +k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= +k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs= +k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA= +k8s.io/apiserver v0.25.0 h1:8kl2ifbNffD440MyvHtPaIz1mw4mGKVgWqM0nL+oyu4= +k8s.io/apiserver v0.25.0/go.mod h1:BKwsE+PTC+aZK+6OJQDPr0v6uS91/HWxX7evElAH6xo= +k8s.io/cli-runtime v0.25.0 h1:XBnTc2Fi+w818jcJGzhiJKQuXl8479sZ4FhtV5hVJ1Q= +k8s.io/cli-runtime v0.25.0/go.mod h1:bHOI5ZZInRHhbq12OdUiYZQN8ml8aKZLwQgt9QlLINw= +k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E= +k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8= +k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= +k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= +k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= +k8s.io/kubectl v0.25.0 h1:/Wn1cFqo8ik3iee1EvpxYre3bkWsGLXzLQI6uCCAkQc= +k8s.io/kubectl v0.25.0/go.mod h1:n16ULWsOl2jmQpzt2o7Dud1t4o0+Y186ICb4O+GwKAU= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4= +oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= +sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= +sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= +sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/packages/testutils/helpers/README.md b/packages/testutils/helpers/README.md new file mode 100644 index 000000000..a1678124e --- /dev/null +++ b/packages/testutils/helpers/README.md @@ -0,0 +1,102 @@ + + +# helpers + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/helpers" +``` + +testhelpers contains helpers for tests + +## Index + +- [Variables](<#variables>) +- [func DiscoveryClient\(\) \(discovery.DiscoveryInterface, error\)](<#DiscoveryClient>) +- [func DynamicClient\(\) \(dynamic.Interface, error\)](<#DynamicClient>) +- [func EnsureNamespace\(ctx context.Context, client kubernetes.Interface, namespace string\) error](<#EnsureNamespace>) +- [func Eventually\(ctx context.Context, condition func\(ctx context.Context\) error, tick time.Duration, msg string, args ...interface\{\}\) error](<#Eventually>) +- [func GVK\(group, version, kind string\) schema.GroupVersionKind](<#GVK>) +- [func KubeClient\(\) \(kubernetes.Interface, error\)](<#KubeClient>) +- [func RestConfig\(\) \(\*rest.Config, error\)](<#RestConfig>) + + +## Variables + + + +```go +var ( + TargetGVK = GVK("fabric.symphony", "v1", "Target") + InstanceGVK = GVK("solution.symphony", "v1", "Instance") + SolutionGVK = GVK("solution.symphony", "v1", "Solution") + ConfigMapGVK = GVK("", "v1", "ConfigMap") + PodGVK = GVK("", "v1", "Pod") + NamespaceGVK = GVK("", "v1", "Namespace") + ClusterRoleGVK = GVK("rbac.authorization.k8s.io", "v1", "ClusterRole") +) +``` + + +## func [DiscoveryClient]() + +```go +func DiscoveryClient() (discovery.DiscoveryInterface, error) +``` + +DiscoveryClient returns the discovery client from the default kube config + + +## func [DynamicClient]() + +```go +func DynamicClient() (dynamic.Interface, error) +``` + +DynamicClient returns the dynamic client from the default kube config + + +## func [EnsureNamespace]() + +```go +func EnsureNamespace(ctx context.Context, client kubernetes.Interface, namespace string) error +``` + +EnsureNamespace ensures that the namespace exists. If it does not exist, it creates it. + + +## func [Eventually]() + +```go +func Eventually(ctx context.Context, condition func(ctx context.Context) error, tick time.Duration, msg string, args ...interface{}) error +``` + + + + +## func [GVK]() + +```go +func GVK(group, version, kind string) schema.GroupVersionKind +``` + +GVK creates a GroupVersionKind + + +## func [KubeClient]() + +```go +func KubeClient() (kubernetes.Interface, error) +``` + +KubeClient returns the kubectl client from the default kube config + + +## func [RestConfig]() + +```go +func RestConfig() (*rest.Config, error) +``` + +RestConfig returns the default kube config + +Generated by [gomarkdoc]() diff --git a/packages/testutils/helpers/eventually.go b/packages/testutils/helpers/eventually.go new file mode 100644 index 000000000..9617ddc4d --- /dev/null +++ b/packages/testutils/helpers/eventually.go @@ -0,0 +1,74 @@ +package helpers + +import ( + "context" + "fmt" + "strings" + "time" +) + +type ( + compoundingError struct { + errors []error + msg string + } +) + +// Error implements error. +func (c *compoundingError) Error() string { + finalMessage := strings.Builder{} + if c.msg != "" { + finalMessage.WriteString(c.msg) + } else { + finalMessage.WriteString("Compounding Error") + } + + if len(c.errors) != 0 { + finalMessage.WriteString(":\n") + } + for _, err := range c.errors { + finalMessage.WriteString(fmt.Sprintf("- %v\n", err)) + } + + return finalMessage.String() +} + +var _ error = &compoundingError{} + +func Eventually(ctx context.Context, condition func(ctx context.Context) error, tick time.Duration, msg string, args ...interface{}) error { + errs := make([]error, 0) + errorBuilder := func() error { + return &compoundingError{ + errors: errs, + msg: fmt.Sprintf(msg, args...), + } + } + + select { + case <-ctx.Done(): + return errorBuilder() + default: + } + + if err := condition(ctx); err == nil { + return nil + } else { + errs = append(errs, err) + } + + ticker := time.NewTicker(tick) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return errorBuilder() + case <-ticker.C: + err := condition(ctx) + if err == nil { + return nil + } + errs = append(errs, err) + } + } +} diff --git a/packages/testutils/helpers/eventually_test.go b/packages/testutils/helpers/eventually_test.go new file mode 100644 index 000000000..ee502e662 --- /dev/null +++ b/packages/testutils/helpers/eventually_test.go @@ -0,0 +1,58 @@ +package helpers + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestEventually_Success(t *testing.T) { + condition := func(ctx context.Context) error { + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + err := Eventually(ctx, condition, 1*time.Millisecond, "failure message") + require.NoError(t, err) +} + +func TestEventually_SuccessfulOnSubsequentAttempt(t *testing.T) { + count := 0 + condition := func(ctx context.Context) error { + defer func() { count++ }() + if count <= 1 { + return errors.New("test error") + } + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + err := Eventually(ctx, condition, 1*time.Millisecond, "failure message") + require.NoError(t, err) + require.Equal(t, 3, count) +} + +func TestEventually_Failure(t *testing.T) { + condition := func(ctx context.Context) error { + return errors.New("test error") + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + err := Eventually(ctx, condition, 1*time.Millisecond, "failure message") + require.Error(t, err) + require.Contains(t, err.Error(), "failure message") + require.Contains(t, err.Error(), "test error") +} + +func TestEventually_FailIfContextIsAlreadyDone(t *testing.T) { + condition := func(ctx context.Context) error { + return nil + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := Eventually(ctx, condition, 1*time.Millisecond, "failure message") + require.Error(t, err) +} diff --git a/packages/testutils/helpers/kubeutil.go b/packages/testutils/helpers/kubeutil.go new file mode 100644 index 000000000..bc38c4f98 --- /dev/null +++ b/packages/testutils/helpers/kubeutil.go @@ -0,0 +1,143 @@ +// testhelpers contains helpers for tests +package helpers + +import ( + "context" + "os" + "path/filepath" + "sync" + + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +var ( + configInst *rest.Config + configInstMutex sync.Mutex + + TargetGVK = GVK("fabric.symphony", "v1", "Target") + InstanceGVK = GVK("solution.symphony", "v1", "Instance") + SolutionGVK = GVK("solution.symphony", "v1", "Solution") + ConfigMapGVK = GVK("", "v1", "ConfigMap") + PodGVK = GVK("", "v1", "Pod") + NamespaceGVK = GVK("", "v1", "Namespace") + ClusterRoleGVK = GVK("rbac.authorization.k8s.io", "v1", "ClusterRole") + + configGetter func(string, string) (*rest.Config, error) = clientcmd.BuildConfigFromFlags + dynamicBuilder func(*rest.Config) (dynamic.Interface, error) = func(config *rest.Config) (dynamic.Interface, error) { + return dynamic.NewForConfig(config) + } + discoveryBuilder func(*rest.Config) (discovery.DiscoveryInterface, error) = func(config *rest.Config) (discovery.DiscoveryInterface, error) { + return discovery.NewDiscoveryClientForConfig(config) + } + kubernetesBuilder func(*rest.Config) (kubernetes.Interface, error) = func(config *rest.Config) (kubernetes.Interface, error) { + return kubernetes.NewForConfig(config) + } +) + +// KubeClient returns the kubectl client from the default kube config +func KubeClient() (kubernetes.Interface, error) { + config, err := RestConfig() + if err != nil { + return nil, err + } + + // create the clientset + clientset, err := kubernetesBuilder(config) + if err != nil { + return nil, err + } + + return clientset, nil +} + +// DiscoveryClient returns the discovery client from the default kube config +func DiscoveryClient() (discovery.DiscoveryInterface, error) { + config, err := RestConfig() + if err != nil { + return nil, err + } + + // create the clientset + client, err := discoveryBuilder(config) + if err != nil { + return nil, err + } + + return client, nil +} + +// DynamicClient returns the dynamic client from the default kube config +func DynamicClient() (dynamic.Interface, error) { + config, err := RestConfig() + if err != nil { + return nil, err + } + + // create the clientset + client, err := dynamicBuilder(config) + if err != nil { + return nil, err + } + + return client, nil +} + +// RestConfig returns the default kube config +func RestConfig() (*rest.Config, error) { + configInstMutex.Lock() + defer configInstMutex.Unlock() + var err error + if configInst != nil { + return configInst, nil + } + homeDir, _ := os.UserHomeDir() + kubeconfigPath := filepath.Join(homeDir, ".kube", "config") + + configInst, err = configGetter("", kubeconfigPath) + if err != nil { + return nil, err + } + return configInst, nil +} + +// EnsureNamespace ensures that the namespace exists. If it does not exist, it creates it. +func EnsureNamespace(ctx context.Context, client kubernetes.Interface, namespace string) error { + _, err := client.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) + if err == nil { + return nil + } + + if kerrors.IsNotFound(err) { + _, err = client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + }, metav1.CreateOptions{}) + if err != nil { + return err + } + + } else { + return err + } + + return nil +} + +// GVK creates a GroupVersionKind +func GVK(group, version, kind string) schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + } +} diff --git a/packages/testutils/helpers/kubeutil_test.go b/packages/testutils/helpers/kubeutil_test.go new file mode 100644 index 000000000..81d85baf7 --- /dev/null +++ b/packages/testutils/helpers/kubeutil_test.go @@ -0,0 +1,181 @@ +package helpers + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/internal" + "github.com/stretchr/testify/require" + "k8s.io/client-go/discovery" + fakedisc "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/dynamic" + fakedyn "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes" + fakekube "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" +) + +type ( + kubeutilTestCase struct { + name string + configError bool + builderErr bool + wantErr bool + } +) + +var ( + matrix = []kubeutilTestCase{ + { + name: "should return error if config builder fails and dynamic builder fails", + configError: true, + builderErr: true, + wantErr: true, + }, + { + name: "should return error if config builder fails", + configError: true, + wantErr: true, + }, + { + name: "should return error if dynamic builder fails", + builderErr: true, + wantErr: true, + }, + { + name: "should return dynamic client", + }, + } + testTimeout = 1 * time.Millisecond +) + +func setDynBuilder(shouldError bool) { + dynamicBuilder = func(*rest.Config) (dynamic.Interface, error) { + if shouldError { + return nil, errors.New("error") + } + return &fakedyn.FakeDynamicClient{}, nil + } +} + +func setConfigBuilder(shouldError bool) { + configGetter = func(string, string) (*rest.Config, error) { + if shouldError { + return nil, errors.New("error") + } + return &rest.Config{}, nil + } +} + +func setDiscoveryBuilder(shouldError bool) { + discoveryBuilder = func(*rest.Config) (discovery.DiscoveryInterface, error) { + if shouldError { + return nil, errors.New("error") + } + return &fakedisc.FakeDiscovery{}, nil + } +} + +func setKubeBuilder(shouldError bool) { + kubernetesBuilder = func(*rest.Config) (kubernetes.Interface, error) { + if shouldError { + return nil, errors.New("error") + } + fakekube.NewSimpleClientset() + return &fakekube.Clientset{}, nil + } +} + +func TestKubernetes(t *testing.T) { + for _, tt := range matrix { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(cleanup) + setKubeBuilder(tt.builderErr) + setConfigBuilder(tt.configError) + _, err := KubeClient() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestDiscovery(t *testing.T) { + for _, tt := range matrix { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(cleanup) + setDiscoveryBuilder(tt.builderErr) + setConfigBuilder(tt.configError) + _, err := DiscoveryClient() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestDynamic(t *testing.T) { + for _, tt := range matrix { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(cleanup) + setDynBuilder(tt.builderErr) + setConfigBuilder(tt.configError) + _, err := DynamicClient() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestRestConfig_Success(t *testing.T) { + t.Cleanup(cleanup) + setConfigBuilder(false) + _, err := RestConfig() + require.NoError(t, err) +} +func TestRestConfig_ReturnsFromCache(t *testing.T) { + t.Cleanup(cleanup) + configInst = &rest.Config{} + setConfigBuilder(false) + returned, err := RestConfig() + require.NoError(t, err) + require.Equal(t, configInst, returned) +} + +func TestRestConfig_Faile(t *testing.T) { + t.Cleanup(cleanup) + setConfigBuilder(true) + _, err := RestConfig() + require.Error(t, err) +} + +func TestEnsureNamespaceSucceedsWhenNamespaceAlreadyExist(t *testing.T) { + t.Cleanup(cleanup) + client := fakekube.NewSimpleClientset(internal.Namespace("test")) // initializes the fake client with the namespace object + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + err := EnsureNamespace(ctx, client, "test") + require.NoError(t, err) +} + +func TestEnsureNamespaceSuccessWhenNamespaceDoesntExist(t *testing.T) { + t.Cleanup(cleanup) + client := fakekube.NewSimpleClientset() + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + err := EnsureNamespace(ctx, client, "test") + require.NoError(t, err) +} + +func cleanup() { + configInst = nil +} diff --git a/packages/testutils/internal/README.md b/packages/testutils/internal/README.md new file mode 100644 index 000000000..00d704a60 --- /dev/null +++ b/packages/testutils/internal/README.md @@ -0,0 +1,125 @@ + + +# internal + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/internal" +``` + +## Index + +- [func GenerateTestApiResourceList\(\) \[\]\*metav1.APIResourceList](<#GenerateTestApiResourceList>) +- [func Namespace\(name string\) \*corev1.Namespace](<#Namespace>) +- [func OutOfSyncResource\(name, namespace string, gvk schema.GroupVersionKind\) \*unstructured.Unstructured](<#OutOfSyncResource>) +- [func Pod\(name, namespace string\) \*corev1.Pod](<#Pod>) +- [func Resource\(name, namespace string, gvk schema.GroupVersionKind\) \*unstructured.Unstructured](<#Resource>) +- [func Target\(name, namespace string\) \*unstructured.Unstructured](<#Target>) +- [type MockT](<#MockT>) + - [func NewMockT\(\) \*MockT](<#NewMockT>) + - [func \(\_m \*MockT\) Errorf\(format string, args ...interface\{\}\)](<#MockT.Errorf>) + - [func \(\_m \*MockT\) Fatalf\(format string, args ...interface\{\}\)](<#MockT.Fatalf>) + - [func \(\_m \*MockT\) Helper\(\)](<#MockT.Helper>) + + + +## func [GenerateTestApiResourceList]() + +```go +func GenerateTestApiResourceList() []*metav1.APIResourceList +``` + + + + +## func [Namespace]() + +```go +func Namespace(name string) *corev1.Namespace +``` + + + + +## func [OutOfSyncResource]() + +```go +func OutOfSyncResource(name, namespace string, gvk schema.GroupVersionKind) *unstructured.Unstructured +``` + + + + +## func [Pod]() + +```go +func Pod(name, namespace string) *corev1.Pod +``` + + + + +## func [Resource]() + +```go +func Resource(name, namespace string, gvk schema.GroupVersionKind) *unstructured.Unstructured +``` + + + + +## func [Target]() + +```go +func Target(name, namespace string) *unstructured.Unstructured +``` + + + + +## type [MockT]() + + + +```go +type MockT struct { + mock.Mock +} +``` + + +### func [NewMockT]() + +```go +func NewMockT() *MockT +``` + + + + +### func \(\*MockT\) [Errorf]() + +```go +func (_m *MockT) Errorf(format string, args ...interface{}) +``` + +Errorf provides a mock function with given fields: format, args + + +### func \(\*MockT\) [Fatalf]() + +```go +func (_m *MockT) Fatalf(format string, args ...interface{}) +``` + + + + +### func \(\*MockT\) [Helper]() + +```go +func (_m *MockT) Helper() +``` + +Helper provides a mock function + +Generated by [gomarkdoc]() diff --git a/packages/testutils/internal/context/README.md b/packages/testutils/internal/context/README.md new file mode 100644 index 000000000..73b1a0e8d --- /dev/null +++ b/packages/testutils/internal/context/README.md @@ -0,0 +1,13 @@ + + +# context + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/internal/context" +``` + +## Index + + + +Generated by [gomarkdoc]() diff --git a/packages/testutils/internal/context/context.go b/packages/testutils/internal/context/context.go new file mode 100644 index 000000000..4937e913a --- /dev/null +++ b/packages/testutils/internal/context/context.go @@ -0,0 +1,56 @@ +package context + +import ( + "context" +) + +type ( + key int + + nestContext struct { + context.Context + level int + } +) + +const ( + nestKey key = iota +) + +func (c *nestContext) Level() int { + return c.level +} + +func (c *nestContext) Nested() *nestContext { + return &nestContext{ + Context: c, + level: c.level + 2, + } +} + +func (c *nestContext) Value(key any) any { + if key == nestKey { + return c + } + return c.Context.Value(key) +} + +// From returns a context.Context that wraps the given context. This context +// allows to track the nesting level of the context. Useful for logging. +func From(ctx context.Context) *nestContext { + if ctx == nil { + return &nestContext{ + Context: context.Background(), + } + } + if ec, ok := ctx.(*nestContext); ok { + return ec + } + ec, ok := ctx.Value(nestKey).(*nestContext) + if !ok { + return &nestContext{ + Context: ctx, + } + } + return ec +} diff --git a/packages/testutils/internal/context/context_test.go b/packages/testutils/internal/context/context_test.go new file mode 100644 index 000000000..4faece490 --- /dev/null +++ b/packages/testutils/internal/context/context_test.go @@ -0,0 +1,37 @@ +package context + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestContext(t *testing.T) { + c, cancel := context.WithTimeout(context.Background(), time.Millisecond) + defer cancel() + + ctx := From(c) + require.Equal(t, 0, ctx.Level()) + + ctx = ctx.Nested() + require.Equal(t, 2, ctx.Level()) + + c, cancel = context.WithTimeout(ctx, time.Millisecond) // embeds regular context + defer cancel() + + c, cancel = context.WithTimeout(c, time.Millisecond) // embeds expectation context further + defer cancel() + + ctx = From(c) + require.Equal(t, 2, ctx.Level()) + + ctx = From(nil) + require.NotNil(t, ctx) + require.Equal(t, 0, ctx.Level()) + + ctx2 := From(ctx) + require.Equal(t, ctx2, ctx) + +} diff --git a/packages/testutils/internal/helper.go b/packages/testutils/internal/helper.go new file mode 100644 index 000000000..f4e401dae --- /dev/null +++ b/packages/testutils/internal/helper.go @@ -0,0 +1,161 @@ +package internal + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func Pod(name, namespace string) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + "management.azure.com/operationId": "test-operation-id", + "test-annotation": "test-annotation-value", + }, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: name, + Image: fmt.Sprintf("image/%s", name), + }, + }, + }, + } +} + +func Resource(name, namespace string, gvk schema.GroupVersionKind) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + "annotations": map[string]interface{}{ + "management.azure.com/operationId": "test-operation-id", + "test-annotation": "test-annotation-value", + }, + }, + "apiVersion": gvk.GroupVersion().String(), + "kind": gvk.Kind, + }, + } +} + +func Target(name, namespace string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + "annotations": map[string]interface{}{ + "management.azure.com/operationId": "test-operation-id", + "test-annotation": "test-annotation-value", + }, + }, + "apiVersion": "fabric.symphony/v1", + "kind": "Target", + "status": map[string]interface{}{ + "provisioningStatus": map[string]interface{}{ + "status": "Succeeded", + "operationId": "test-operation-id", + }, + }, + }, + } +} + +func Namespace(name string) *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + } +} + +func OutOfSyncResource(name, namespace string, gvk schema.GroupVersionKind) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + "annotations": map[string]interface{}{ + "management.azure.com/operationId": "test-operation-new", + "test-annotation": "test-annotation-value", + }, + }, + "apiVersion": gvk.GroupVersion().String(), + "kind": gvk.Kind, + "status": map[string]interface{}{ + "provisioningStatus": map[string]interface{}{ + "status": "Succeeded", + "operationId": "test-operation-id-old", // this is the old operation id + }, + }, + }, + } +} + +func GenerateTestApiResourceList() []*metav1.APIResourceList { + // we want to test the following resources: + // - core/v1: Pods, ConfigMaps, Namespaces + // - orchestrator.iotoperations.azure.com/v1: Targets + + return []*metav1.APIResourceList{ + { + GroupVersion: "v1", + APIResources: []metav1.APIResource{ + { + Name: "pods", + Namespaced: true, + Kind: "Pod", + SingularName: "pod", + Group: "", + Version: "v1", + }, + { + Name: "configmaps", + Namespaced: true, + Kind: "ConfigMap", + SingularName: "configmap", + Group: "", + Version: "v1", + }, + { + Name: "namespaces", + Namespaced: false, + Kind: "Namespace", + SingularName: "namespace", + Group: "", + Version: "v1", + }, + }, + }, + { + GroupVersion: "fabric.symphony/v1", + APIResources: []metav1.APIResource{ + { + Name: "targets", + Namespaced: true, + Kind: "Target", + SingularName: "target", + Group: "fabric.symphony", + Version: "v1", + }, + }, + }, + } +} diff --git a/packages/testutils/internal/helper_test.go b/packages/testutils/internal/helper_test.go new file mode 100644 index 000000000..b62c82540 --- /dev/null +++ b/packages/testutils/internal/helper_test.go @@ -0,0 +1,58 @@ +package internal + +import ( + "testing" + + "github.com/eclipse-symphony/symphony/packages/testutils/helpers" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestPod(t *testing.T) { + p := Pod("test-pod", "test-namespace") + require.Equal(t, "test-pod", p.GetName()) + require.Equal(t, "test-namespace", p.GetNamespace()) + require.Equal(t, helpers.PodGVK, p.GroupVersionKind()) +} + +func TestTarget(t *testing.T) { + tgt := Target("test", "test-namespace") + require.Equal(t, "test", tgt.GetName()) + require.Equal(t, "test-namespace", tgt.GetNamespace()) + require.Equal(t, helpers.TargetGVK, tgt.GroupVersionKind()) +} +func TestOutOfSyncTarget(t *testing.T) { + tgt := OutOfSyncResource("test", "test-namespace", helpers.TargetGVK) + require.Equal(t, "test", tgt.GetName()) + require.Equal(t, "test-namespace", tgt.GetNamespace()) + require.Equal(t, helpers.TargetGVK, tgt.GroupVersionKind()) +} + +func TestApiResourceListGenerator(t *testing.T) { + arl := GenerateTestApiResourceList() + require.Len(t, arl, 2) +} + +func TestResource(t *testing.T) { + r := Resource("test", "test-namespace", helpers.InstanceGVK) + require.Equal(t, "test", r.GetName()) + require.Equal(t, "test-namespace", r.GetNamespace()) + require.Equal(t, helpers.InstanceGVK, r.GroupVersionKind()) +} + +func TestNamespace(t *testing.T) { + ns := Namespace("test") + require.Equal(t, "test", ns.GetName()) + require.Equal(t, helpers.NamespaceGVK, ns.GroupVersionKind()) +} + +func TestMockT(t *testing.T) { + m := NewMockT() + m.On("Helper") + m.On("Errorf", mock.Anything, mock.Anything) + m.On("Fatalf", mock.Anything, mock.Anything) + m.Helper() + m.Errorf("test") + m.Fatalf("test") + m.AssertExpectations(t) +} diff --git a/packages/testutils/internal/mockt.go b/packages/testutils/internal/mockt.go new file mode 100644 index 000000000..268cd9849 --- /dev/null +++ b/packages/testutils/internal/mockt.go @@ -0,0 +1,25 @@ +package internal + +import "github.com/stretchr/testify/mock" + +type MockT struct { + mock.Mock +} + +func NewMockT() *MockT { + return &MockT{} +} + +// Errorf provides a mock function with given fields: format, args +func (_m *MockT) Errorf(format string, args ...interface{}) { + _m.Called(format, args) +} + +func (_m *MockT) Fatalf(format string, args ...interface{}) { + _m.Called(format, args) +} + +// Helper provides a mock function +func (_m *MockT) Helper() { + _m.Called() +} diff --git a/packages/testutils/logger/README.md b/packages/testutils/logger/README.md new file mode 100644 index 000000000..bdb524170 --- /dev/null +++ b/packages/testutils/logger/README.md @@ -0,0 +1,43 @@ + + +# logger + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/logger" +``` + +## Index + +- [func SetDefaultLogger\(logger Logger\)](<#SetDefaultLogger>) +- [type Logger](<#Logger>) + - [func GetDefaultLogger\(\) Logger](<#GetDefaultLogger>) + + + +## func [SetDefaultLogger]() + +```go +func SetDefaultLogger(logger Logger) +``` + + + + +## type [Logger]() + + + +```go +type Logger = func(format string, args ...interface{}) +``` + + +### func [GetDefaultLogger]() + +```go +func GetDefaultLogger() Logger +``` + + + +Generated by [gomarkdoc]() diff --git a/packages/testutils/logger/logger.go b/packages/testutils/logger/logger.go new file mode 100644 index 000000000..4f5f17897 --- /dev/null +++ b/packages/testutils/logger/logger.go @@ -0,0 +1,19 @@ +package logger + +import "fmt" + +type ( + Logger = func(format string, args ...interface{}) +) + +var defaultLogger Logger = func(format string, args ...interface{}) { + fmt.Printf(format, args...) +} + +func SetDefaultLogger(logger Logger) { + defaultLogger = logger +} + +func GetDefaultLogger() Logger { + return defaultLogger +} diff --git a/packages/testutils/logger/logger_test.go b/packages/testutils/logger/logger_test.go new file mode 100644 index 000000000..db3b512e4 --- /dev/null +++ b/packages/testutils/logger/logger_test.go @@ -0,0 +1,23 @@ +package logger + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDefault(t *testing.T) { + require.NotPanics(t, func() { + GetDefaultLogger()("test") + }) +} + +func TestCustomLogger(t *testing.T) { + called := false + fn := func(string, ...interface{}) { + called = true + } + SetDefaultLogger(fn) + GetDefaultLogger()("test") + require.True(t, called) +} diff --git a/packages/testutils/magefile.go b/packages/testutils/magefile.go new file mode 100644 index 000000000..8c9c13dc0 --- /dev/null +++ b/packages/testutils/magefile.go @@ -0,0 +1,9 @@ +//go:build mage +// +build mage + +package main + +import ( + // mage:import + _ "github.com/eclipse-symphony/symphony/packages/mage" +) diff --git a/packages/testutils/types/README.md b/packages/testutils/types/README.md new file mode 100644 index 000000000..e7c9335f7 --- /dev/null +++ b/packages/testutils/types/README.md @@ -0,0 +1,68 @@ + + +# types + +```go +import "github.com/eclipse-symphony/symphony/packages/testutils/types" +``` + +## Index + +- [type Condition](<#Condition>) +- [type Expectation](<#Expectation>) +- [type GomegaEventuallySubject](<#GomegaEventuallySubject>) +- [type GomegaMatchable](<#GomegaMatchable>) + + + +## type [Condition]() + +Condition is a way to define a condition for a resource. It is used to verify whether a resource is in the expected state. It's meant to be used in conjunction with an Expectation. The Expectation is responsible for retrieving the resource and passing it to the Condition to verify that the resource is as expected. + +```go +type Condition interface { + + // IsSatisfiedBy returns `nil` if the condition is satisfied by the given resource. + IsSatisfiedBy(ctx context.Context, resource interface{}) error + // contains filtered or unexported methods +} +``` + + +## type [Expectation]() + +Expectation is a way to define expectations for a resource. It is used to verify whether a resource is in the expected state. It's responsible for retrieving the resource and verifying that the resource is as expected. + +```go +type Expectation interface { + + // Verify runs the expactation. It returns an error if the expectation is not met. + Verify(ctx context.Context) error + // contains filtered or unexported methods +} +``` + + +## type [GomegaEventuallySubject]() + + + +```go +type GomegaEventuallySubject interface { + GomegaMatchable + AsGomegaSubject() func(context.Context) (interface{}, error) +} +``` + + +## type [GomegaMatchable]() + + + +```go +type GomegaMatchable interface { + ToGomegaMatcher() types.GomegaMatcher +} +``` + +Generated by [gomarkdoc]() diff --git a/packages/testutils/types/types.go b/packages/testutils/types/types.go new file mode 100644 index 000000000..e0a6850ee --- /dev/null +++ b/packages/testutils/types/types.go @@ -0,0 +1,39 @@ +package types + +import ( + "context" + + "github.com/onsi/gomega/types" +) + +type ( + id interface { + Id() string + Description() string + } + + // Expectation is a way to define expectations for a resource. It is used to verify whether a resource is in the + // expected state. It's responsible for retrieving the resource and verifying that the resource is as expected. + Expectation interface { + id + // Verify runs the expactation. It returns an error if the expectation is not met. + Verify(ctx context.Context) error + } + + // Condition is a way to define a condition for a resource. It is used to verify whether a resource is in the + // expected state. It's meant to be used in conjunction with an Expectation. The Expectation is responsible for + // retrieving the resource and passing it to the Condition to verify that the resource is as expected. + Condition interface { + id + // IsSatisfiedBy returns `nil` if the condition is satisfied by the given resource. + IsSatisfiedBy(ctx context.Context, resource interface{}) error + } + + GomegaMatchable interface { + ToGomegaMatcher() types.GomegaMatcher + } + GomegaEventuallySubject interface { + GomegaMatchable + AsGomegaSubject() func(context.Context) (interface{}, error) + } +) diff --git a/test/integration/go.mod b/test/integration/go.mod index 95fa581c8..f21eb13b1 100644 --- a/test/integration/go.mod +++ b/test/integration/go.mod @@ -2,58 +2,156 @@ module github.com/eclipse-symphony/symphony/test/integration go 1.20 -replace github.com/eclipse-symphony/symphony/coa => ../../coa - -replace github.com/eclipse-symphony/symphony/api => ../../api +replace ( + github.com/eclipse-symphony/symphony/api => ../../api + github.com/eclipse-symphony/symphony/coa => ../../coa + github.com/eclipse-symphony/symphony/k8s => ../../k8s + github.com/eclipse-symphony/symphony/packages/mage => ../../packages/mage + github.com/eclipse-symphony/symphony/packages/testutils => ../../packages/testutils +) require ( + github.com/eclipse-symphony/symphony/api v0.0.0-00010101000000-000000000000 + github.com/eclipse-symphony/symphony/coa v0.0.0 + github.com/eclipse-symphony/symphony/packages/testutils v0.0.0-00010101000000-000000000000 + github.com/onsi/ginkgo/v2 v2.13.1 + github.com/onsi/gomega v1.30.0 github.com/princjef/mageutil v1.0.0 github.com/stretchr/testify v1.8.4 gopkg.in/yaml.v2 v2.4.0 - k8s.io/apimachinery v0.25.0 + k8s.io/apimachinery v0.25.2 k8s.io/client-go v0.25.0 ) require ( + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/BurntSushi/toml v1.1.0 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.2 // indirect + github.com/Masterminds/squirrel v1.5.3 // indirect + github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/containerd/containerd v1.7.0-beta.0 // indirect + github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/docker/cli v24.0.6+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v20.10.24+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.10.1 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/color v1.13.0 // indirect + github.com/go-errors/errors v1.0.1 // indirect + github.com/go-gorp/gorp/v3 v3.0.2 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.1 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/btree v1.0.1 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.4.0 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/itchyny/gojq v0.12.13 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.3 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lib/pq v1.10.6 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc5 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rubenv/sql-migrate v1.1.2 // indirect + github.com/russross/blackfriday v1.6.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xlab/treeprint v1.1.0 // indirect + go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect + golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/term v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/tools v0.14.0 // indirect google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect + google.golang.org/grpc v1.61.1 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.25.0 // indirect + helm.sh/helm/v3 v3.10.0 // indirect + k8s.io/api v0.25.2 // indirect + k8s.io/apiextensions-apiserver v0.25.0 // indirect + k8s.io/apiserver v0.25.0 // indirect + k8s.io/cli-runtime v0.25.0 // indirect + k8s.io/component-base v0.25.0 // indirect k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect - k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect + k8s.io/kubectl v0.25.0 // indirect + k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect + oras.land/oras-go v1.2.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/kustomize/api v0.12.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect -) \ No newline at end of file +) diff --git a/test/integration/go.sum b/test/integration/go.sum index 492a0444e..a7754adeb 100644 --- a/test/integration/go.sum +++ b/test/integration/go.sum @@ -1,23 +1,175 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= +github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/hcsshim v0.10.0-rc.7 h1:HBytQPxcv8Oy4244zbQbe6hnOnx544eL5QPUqhJldz8= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/cheggaaa/pb v2.0.7+incompatible/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/cheggaaa/pb/v3 v3.0.4/go.mod h1:7rgWxLrAUcFMkvJuv09+DYi7mMUYi8nO9iOWcvGJPfw= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/containerd/cgroups v1.0.5-0.20220816231112-7083cd60b721 h1:qWq0iv560E8jXZKwWipx3Xot0dYPyfKBeDNfRwYth/U= +github.com/containerd/containerd v1.7.0-beta.0 h1:TmelrlMneeWvAbqqTB9XQ3yCc3voPrBT/k80D8kj5dw= +github.com/containerd/containerd v1.7.0-beta.0/go.mod h1:d+x3kmR4hnXSGTCbLRpBFnP5lOEjqm7dLwZ4UCz01WI= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk= +github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY= +github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= +github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= +github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= +github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4= +github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -27,242 +179,887 @@ github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTr github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= +github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= +github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= +github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= +github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= +github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= +github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= 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/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= +github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 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/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/matryer/is v1.3.0 h1:9qiso3jaJrOe6qBRJRBt2Ldht05qDiFP9le0JOIhRSI= github.com/matryer/is v1.3.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= -github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= +github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU= +github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= github.com/princjef/mageutil v1.0.0 h1:1OfZcJUMsooPqieOz2ooLjI+uHUo618pdaJsbCXcFjQ= github.com/princjef/mageutil v1.0.0/go.mod h1:mkShhaUomCYfAoVvTKRcbAs8YSVPdtezI5j6K+VXhrs= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36tZeQ= +github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 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/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= +github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI= +golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 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/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 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/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/VividCortex/ewma.v1 v1.1.1/go.mod h1:TekXuFipeiHWiAlO1+wSS23vTcyFau5u3rxXUSXj710= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/cheggaaa/pb.v2 v2.0.7/go.mod h1:0CiZ1p8pvtxBlQpLXkHuUTpdJ1shm3OqCF1QugkjHL4= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fatih/color.v1 v1.7.0/go.mod h1:P7yosIhqIl/sX8J8UypY5M+dDpD2KmyfP5IRs5v/fo0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mattn/go-colorable.v0 v0.1.0/go.mod h1:BVJlBXzARQxdi3nZo6f6bnl5yR20/tOL6p+V0KejgSY= gopkg.in/mattn/go-isatty.v0 v0.0.4/go.mod h1:wt691ab7g0X4ilKZNmMII3egK0bTxl37fEn/Fwbd8gc= gopkg.in/mattn/go-runewidth.v0 v0.0.4/go.mod h1:BmXejnxvhwdaATwiJbB1vZ2dtXkQKZGu9yLFCZb4msQ= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/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= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +helm.sh/helm/v3 v3.10.0 h1:y/MYONZ/bsld9kHwqgBX2uPggnUr5hahpjwt9/jrHlI= +helm.sh/helm/v3 v3.10.0/go.mod h1:paPw0hO5KVfrCMbi1M8+P8xdfBri3IiJiVKATZsFR94= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk= -k8s.io/api v0.27.3 h1:yR6oQXXnUEBWEWcvPWS0jQL575KoAboQPfJAuKNrw5Y= -k8s.io/api v0.27.3/go.mod h1:C4BNvZnQOF7JA/0Xed2S+aUyJSfTGkGFxLXz9MnpIpg= -k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= -k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= -k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM= -k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.25.2 h1:v6G8RyFcwf0HR5jQGIAYlvtRNrxMJQG1xJzaSeVnIS8= +k8s.io/api v0.25.2/go.mod h1:qP1Rn4sCVFwx/xIhe+we2cwBLTXNcheRyYXwajonhy0= +k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= +k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= +k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs= +k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA= +k8s.io/apiserver v0.25.0 h1:8kl2ifbNffD440MyvHtPaIz1mw4mGKVgWqM0nL+oyu4= +k8s.io/apiserver v0.25.0/go.mod h1:BKwsE+PTC+aZK+6OJQDPr0v6uS91/HWxX7evElAH6xo= +k8s.io/cli-runtime v0.25.0 h1:XBnTc2Fi+w818jcJGzhiJKQuXl8479sZ4FhtV5hVJ1Q= +k8s.io/cli-runtime v0.25.0/go.mod h1:bHOI5ZZInRHhbq12OdUiYZQN8ml8aKZLwQgt9QlLINw= k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E= k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8= -k8s.io/client-go v0.27.3 h1:7dnEGHZEJld3lYwxvLl7WoehK6lAq7GvgjxpA3nv1E8= -k8s.io/client-go v0.27.3/go.mod h1:2MBEKuTo6V1lbKy3z1euEGnhPfGZLKTS9tiJ2xodM48= +k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= +k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk= k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= -k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= -k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kubectl v0.25.0 h1:/Wn1cFqo8ik3iee1EvpxYre3bkWsGLXzLQI6uCCAkQc= +k8s.io/kubectl v0.25.0/go.mod h1:n16ULWsOl2jmQpzt2o7Dud1t4o0+Y186ICb4O+GwKAU= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4= +oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= +sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= +sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= +sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= \ No newline at end of file +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/test/integration/lib/shell/shell.go b/test/integration/lib/shell/shell.go new file mode 100644 index 000000000..89edca00b --- /dev/null +++ b/test/integration/lib/shell/shell.go @@ -0,0 +1,79 @@ +package shell + +import ( + "context" + "fmt" + "os/exec" + "path/filepath" + "runtime" + + ginkgo "github.com/onsi/ginkgo/v2" +) + +var localenvPath string + +func init() { + _, filename, _, _ := runtime.Caller(0) + dir := filepath.Dir(filename) + localenvPath = filepath.Join(dir, "../../../localenv") +} + +// Run a command in the context of /localenv +func LocalenvCmd(ctx context.Context, cmd string) error { + // first print the working directory + return Exec(ctx, fmt.Sprintf("cd %s && %s", localenvPath, cmd)) +} + +// Run a command with | or other things that do not work in shellcmd +func Exec(ctx context.Context, cmd string) error { + execCmd := getShellCmd(ctx, cmd) + return execCmd.Run() +} + +func ExecAll(ctx context.Context, cmds ...string) error { + for _, cmd := range cmds { + err := Exec(ctx, cmd) + if err != nil { + return err + } + } + return nil +} + +func Output(ctx context.Context, cmd string) ([]byte, error) { + execCmd := getShellCmd(ctx, cmd) + + return execCmd.Output() +} + +func PipeInExec(ctx context.Context, cmd string, stdin []byte) error { + execCmd := getShellCmd(ctx, cmd) + writer, err := execCmd.StdinPipe() + if err != nil { + return err + } + writer.Write(stdin) + writer.Close() + + return execCmd.Run() +} + +func PipeInForOutput(ctx context.Context, cmd string, stdin []byte) ([]byte, error) { + execCmd := getShellCmd(ctx, cmd) + writer, err := execCmd.StdinPipe() + if err != nil { + return nil, err + } + writer.Write(stdin) + writer.Close() + + return execCmd.Output() +} + +func getShellCmd(ctx context.Context, cmd string) *exec.Cmd { + ginkgo.GinkgoWriter.Printf("\033[35m>\033[0m\033[1m %s\033[0m\n", cmd) + execCmd := exec.CommandContext(ctx, "sh", "-c", cmd) + execCmd.Stdout = ginkgo.GinkgoWriter + execCmd.Stderr = ginkgo.GinkgoWriter + return execCmd +} diff --git a/test/integration/lib/testhelpers/component_map.go b/test/integration/lib/testhelpers/component_map.go new file mode 100644 index 000000000..9d8c3f516 --- /dev/null +++ b/test/integration/lib/testhelpers/component_map.go @@ -0,0 +1,558 @@ +package testhelpers + +type Map = map[string]interface{} +type Array = []interface{} + +// Todo: Switch over to symphony core types from the /k8s/api folder +var ( + ComponetsMap = map[string]ComponentSpec{ + // A simple chart that deploy a single pod, a configmap and a serviceaccount + "simple-chart-1": { + Name: "simple-chart-1", + Properties: Map{ + "chart": Map{ + "repo": "symphonycr.azurecr.io/simple-chart", + "version": "0.3.0", + }, + }, + Type: "helm.v3", + }, + + "simple-chart-1-nonexistent": { + Name: "simple-chart-1", + Properties: Map{ + "chart": Map{ + "repo": "symphonycr.azurecr.io/simple-chart", + "version": "0.3.0-nonexistent", + }, + }, + Type: "helm.v3", + }, + + "simple-chart-1-with-values": { + Name: "simple-chart-1", + Properties: Map{ + "chart": Map{ + "repo": "symphonycr.azurecr.io/simple-chart", + "version": "0.3.0", + }, + "values": Map{ + "configData": Map{ + "key": "value", + }, + }, + }, + Type: "helm.v3", + }, + + // A simple chart that deploy a single pod, a configmap and a serviceaccount + "simple-chart-2": { + Name: "simple-chart-2", + Properties: Map{ + "chart": Map{ + "repo": "symphonycr.azurecr.io/simple-chart", + "version": "0.3.0", + }, + }, + Type: "helm.v3", + }, + + // A non-exisitent chart + "simple-chart-2-nonexistent": { + Name: "simple-chart-2", + Properties: Map{ + "chart": Map{ + "repo": "symphonycr.azurecr.io/simple-chart", + "version": "0.3.0-non-existent", + }, + }, + Type: "helm.v3", + }, + + "mongodb-configmap": { + Name: "mongodb", + Type: "yaml.k8s", + Properties: Map{ + "resource": Map{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": Map{ + "name": "mongodb", + }, + "data": Map{ + "database": "mongodb", + "database_uri": "mongodb://localhost:27017", + }, + }, + }, + }, + + "mongodb-configmap-modified": { + Name: "mongodb", + Type: "yaml.k8s", + Properties: Map{ + "resource": Map{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": Map{ + "name": "mongodb", + }, + "data": Map{ + "database": "mongodb", + "database_uri": "mongodb://localhost:27020", // changed port + }, + }, + }, + }, + + "mongodb-constraint": { + Name: "mongodb-constraint", + Type: "yaml.k8s", + Properties: Map{ + "resource": Map{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": Map{ + "name": "mongodb-constraint", + }, + "data": Map{ + "database": "mongodb", + "database_uri": "mongodb://localhost:27017", + }, + }, + }, + Constraints: "${{$equal($property('OS'),'windows')}}", + }, + + "nginx": { + Name: "nginx", + Properties: Map{ + "resource": Map{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": Map{ + "name": "nginx", + }, + "spec": Map{ + "replicas": int64(1), + "selector": Map{ + "matchLabels": Map{ + "app": "nginx", + }, + }, + "template": Map{ + "metadata": Map{ + "labels": Map{ + "app": "nginx", + }, + }, + "spec": Map{ + "containers": []Map{ + { + "image": "nginx:1.21", + "name": "nginx", + "ports": Array{ + Map{"containerPort": int64(80)}, + }, + }, + }, + }, + }, + }, + }, + }, + Type: "yaml.k8s", + }, + + "basic-clusterrole": { + Name: "basic-clusterrole", + Properties: Map{ + "resource": Map{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": Map{ + "name": "basic-clusterrole", + }, + "rules": Array{ + Map{ + "apiGroups": Array{ + "apps", + }, + "resources": Array{ + "deployments", + }, + "verbs": Array{ + "get", + "list", + "watch", + "create", + "update", + }, + }, + }, + }, + }, + Type: "yaml.k8s", + }, + + "basic-configmap-1": { + Name: "basic-configmap-1", + Properties: Map{ + "resource": Map{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": Map{ + "name": "basic-configmap-1", + }, + "data": Map{ + "key": "value", + }, + }, + }, + Type: "yaml.k8s", + }, + + "basic-configmap-1-modified": { + Name: "basic-configmap-1", + Properties: Map{ + "resource": Map{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": Map{ + "name": "basic-configmap-1", + }, + "data": Map{ + "key": "value-modified", + }, + }, + }, + Type: "yaml.k8s", + }, + "basic-configmap-1-params": { + Name: "basic-configmap-1", + Properties: Map{ + "resource": Map{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": Map{ + "name": "basic-configmap-1", + }, + "data": Map{ + "database": "@{{ $param('database')}}", + "database_uri": "@{{ $param('database_uri')}}", + }, + }, + }, + Type: "yaml.k8s", + }, + "basic-configmap-1-params-modified": { + Name: "basic-configmap-1", + Properties: Map{ + "resource": Map{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": Map{ + "name": "basic-configmap-1", + }, + "data": Map{ + "uri": "${{ $param('test')}}", + }, + }, + }, + Type: "yaml.k8s", + }, + "foobar-crd": { + Name: "foobar-crd", + Properties: Map{ + "resource": Map{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": Map{ + "name": "foobars.contoso.io", + }, + "spec": Map{ + "group": "contoso.io", + "version": "v1", + "scope": "Namespaced", + "names": Map{ + "plural": "foobars", + "singular": "foobar", + "kind": "FooBar", + "shortNames": Array{ + "fb", + }, + }, + "versions": Array{ + Map{ + "name": "v1", + "served": true, + "storage": true, + "schema": Map{ + "openAPIV3Schema": Map{ + "type": "object", + "properties": Map{ + "spec": Map{ + "type": "object", + "properties": Map{ + "foo": Map{ + "type": "string", + }, + "bar": Map{ + "type": "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "statusProbe": Map{ + "succeededValues": Array{"True"}, + "statusPath": `$.status.conditions[?(@.type == "Established")].status`, + "initialWait": "5s", + }, + }, + Type: "yaml.k8s", + }, + "simple-foobar": { + Name: "simple-foobar", + Properties: Map{ + "resource": Map{ + "apiVersion": "contoso.io/v1", + "kind": "FooBar", + "metadata": Map{ + "name": "simple-foobar", + }, + "spec": Map{ + "foo": "foo", + "bar": "bar", + }, + }, + }, + Type: "yaml.k8s", + Dependencies: []string{ + "foobar-crd", + }, + }, + + // A simple chart with a simple templated expression. + "expressions-1": { + Name: "expressions-1", + Properties: Map{ + "chart": Map{ + "repo": "symphonycr.azurecr.io/simple-chart", + "version": "0.3.0", + }, + "foo": `@{{ $property("color") + ' ' + $property("OS") }}`, + "testGtNumbers": `@{{ $gt("2", 1.0)}}`, + "testGeNumbers": `@{{ $ge(2, "1.0")}}`, + "testLtNumbers": `@{{ $lt("2", 1.0)}}`, + "testLeNumbers": `@{{ $le(2, "1.0")}}`, + "testBetweenNumbers": `@{{ $between(2, "1", 3)}}`, + }, + Type: "helm.v3", + }, + + // A simple chart with an invalid templated expression, $property("will-fail") does not exist on the target. + "expressions-1-failed": { + Name: "expressions-1", + Properties: Map{ + "chart": Map{ + "repo": "symphonycr.azurecr.io/simple-chart", + "version": "0.3.0", + }, + "foo": `${{ $property("will-fail") + ' ' + $property("OS") }}`, + "testGtNumbers": `${{ $gt("2", 1.0)}}`, + "testGeNumbers": `${{ $ge(2, "1.0")}}`, + "testLtNumbers": `${{ $lt("2", 1.0)}}`, + "testLeNumbers": `${{ $le(2, "1.0")}}`, + "testBetweenNumbers": `${{ $between(2, "1", 3)}}`, + }, + Type: "helm.v3", + }, + + // A simple chart with a simple templated expression. + "expressions-1-soln": { + Name: "expressions-1-soln", + Properties: Map{ + "resource": Map{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": Map{ + "name": "expressions-1-soln", + "foo": `@{{ $property("color") + ' ' + $property("OS") }}`, + "normalString": `This is interpreted as a normal string @{{ $property("wont-fail") }}`, + "testEqualNumbers": `@{{ $equal(123, 123) }}`, + "testNotTrue": `@{{ $not("true")}}`, + "testNotNotTrue": `@{{ $not($not(true))}}`, + "testPropertyAnd": `@{{ $and($equal($property("OS"), "windows") , $equal("yes", "no"))}}`, + "testPropertyOr": `@{{ $or($equal($property("OS"), "windows") , $equal("yes", "no"))}}`, + }, + }, + }, + Type: "yaml.k8s", + }, + + // A simple chart with an invalid templated expression, $property("will-fail") does not exist on the target. + "expressions-1-soln-failed": { + Name: "expressions-1-soln", + Properties: Map{ + "chart": Map{ + "repo": "symphonycr.azurecr.io/simple-chart", + "version": "0.3.0", + }, + "name": "expressions-1-soln", + "foo": `@{{ $property("will-fail") + ' ' + $property("OS") }}`, + "normalString": `This is interpreted as a normal string @{{ $property("wont-fail") }}`, + "testEqualNumbers": `@{{ $equal(123, 123) }}`, + "testNotTrue": `@{{ $not("true")}}`, + "testNotNotTrue": `@{{ $not($not(true))}}`, + "testPropertyAnd": `@{{ $and($equal($property("OS"), "windows") , $equal("yes", "no"))}}`, + "testPropertyOr": `@{{ $or($equal($property("OS"), "windows") , $equal("yes", "no"))}}`, + }, + Type: "helm.v3", + }, + + "simple-http": { + Name: "simple-http", + Properties: Map{ + "http.url": "https://learn.microsoft.com/en-us/content-nav/azure.json?", + "http.method": "GET", + }, + Type: "http", + }, + "simple-http-invalid-url": { + Name: "simple-http", + Properties: Map{ + "http.url": "https://learn.microsoft.com/en-us/test/invalid/url", + "http.method": "GET", + }, + Type: "http", + }, + "e4k": { + Name: "e4k", + Properties: map[string]interface{}{ + "chart": map[string]interface{}{ + "repo": "e4kpreview.azurecr.io/helm/az-e4k", + "version": "0.3.0", + }, + }, + Type: "helm.v3", + }, + "e4k-broker": { + Name: "e4k-high-availability-broker", + Properties: map[string]interface{}{ + "chart": map[string]interface{}{ + "repo": "symphonycr.azurecr.io/az-e4k-broker", + "version": "0.1.0", + }, + }, + Type: "helm.v3", + }, + "bluefin-extension": { + Name: "bluefin", + Properties: map[string]interface{}{ + "chart": map[string]interface{}{ + "repo": "azbluefin.azurecr.io/helm/bluefin-arc-extension", + "version": "0.2.0-20230706.3-develop", + }, + }, + Type: "helm.v3", + }, + "bluefin-instance": { + Name: "bluefin-instance", + Properties: map[string]interface{}{ + "resource": map[string]interface{}{ + "apiVersion": "bluefin.az-bluefin.com/v1", + "kind": "Instance", + "metadata": map[string]interface{}{ + "name": "bf-instance", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "displayName": "Test Instance", + "otelCollectorAddress": "otel-collector.alice-springs.svc.cluster.local:4317", + }, + }, + }, + Type: "yaml.k8s", + }, + + "bluefin-pipeline": { + Name: "test-pipeline", + Properties: map[string]interface{}{ + "resource": map[string]interface{}{ + "apiVersion": "bluefin.az-bluefin.com/v1", + "kind": "Pipeline", + "metadata": map[string]interface{}{ + "name": "bf-pipeline", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "displayName": "bf-pipeline", + "enabled": true, + "input": map[string]interface{}{ + "description": "Read from topic Thermostat 3", + "displayName": "E4K", + "format": map[string]interface{}{"type": "json"}, + "mqttConnectionInfo": map[string]interface{}{ + "broker": "tcp://azedge-dmqtt-frontend:1883", + "password": "password", + "username": "client1", + }, + "next": []interface{}{"node-22f2"}, + "topics": []interface{}{ + map[string]interface{}{ + "name": "alice-springs/data/opc-ua-connector/opc-ua-connector/thermostat-sample-3", + }, + }, + "type": "input/mqtt@v1", + "viewOptions": map[string]interface{}{ + "position": map[string]interface{}{ + "x": 0, + "y": 80, + }, + }, + }, + "partitionCount": 6, + "stages": map[string]interface{}{ + "node-22f2": map[string]interface{}{ + "displayName": "No-op", + "next": []interface{}{"output"}, + "query": ".", + "type": "processor/transform@v1", + "viewOptions": map[string]interface{}{ + "position": map[string]interface{}{ + "x": 0, + "y": 208, + }, + }, + }, + "output": map[string]interface{}{ + "broker": "tcp://azedge-dmqtt-frontend:1883", + "description": "Publish to topic demo-output-topic", + "displayName": "E4K", + "format": map[string]interface{}{"type": "json"}, + "password": "password", + "timeout": "45ms", + "topic": "alice-springs/data/demo-output", + "type": "output/mqtt@v1", + "username": "client1", + "viewOptions": map[string]interface{}{ + "position": map[string]interface{}{ + "x": 0, + "y": 336, + }, + }, + }, + }, + }, + }, + }, + Type: "yaml.k8s", + }, + } +) diff --git a/test/integration/lib/testhelpers/helmvalues.go b/test/integration/lib/testhelpers/helmvalues.go new file mode 100644 index 000000000..059035a67 --- /dev/null +++ b/test/integration/lib/testhelpers/helmvalues.go @@ -0,0 +1,49 @@ +package testhelpers + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +type ( + HelmValues map[string]interface{} +) + +func (h HelmValues) String() string { + flatMap := make(map[string]interface{}, len(h)) + flatten(reflect.ValueOf(h), "", flatMap) + + items := make([]string, 0, len(flatMap)) + for key, value := range flatMap { + switch v := value.(type) { + case string: + value = fmt.Sprintf(`"%s"`, strings.ReplaceAll(v, `"`, `\"`)) + } + items = append(items, fmt.Sprintf("--set %s=%v", key, value)) + } + return strings.Join(items, " ") +} + +func flatten(inputValue reflect.Value, parentKey string, flatMap map[string]interface{}) { + if inputValue.Kind() == reflect.Interface || inputValue.Kind() == reflect.Ptr { + inputValue = inputValue.Elem() + } + inputType := inputValue.Type() + + switch inputType.Kind() { + case reflect.Map: + for _, key := range inputValue.MapKeys() { + flatten(inputValue.MapIndex(key), parentKey+"."+key.String(), flatMap) + } + case reflect.Slice: + for i := 0; i < inputValue.Len(); i++ { + flatten(inputValue.Index(i), parentKey+"["+strconv.Itoa(i)+"]", flatMap) + } + default: + parentKey = strings.TrimPrefix(parentKey, ".") + inputValue.Type() + flatMap[parentKey] = inputValue.Interface() + } +} diff --git a/test/integration/lib/testhelpers/helpers.go b/test/integration/lib/testhelpers/helpers.go new file mode 100644 index 000000000..73a39bfbf --- /dev/null +++ b/test/integration/lib/testhelpers/helpers.go @@ -0,0 +1,25 @@ +package testhelpers + +import ( + "context" + + "github.com/eclipse-symphony/symphony/test/integration/lib/shell" +) + +func DumpClusterState(ctx context.Context) { + shell.Exec(ctx, "kubectl get all -A -o wide") + shell.Exec(ctx, "kubectl get events -A --sort-by=.metadata.creationTimestamp") + shell.Exec(ctx, "kubectl get targets.fabric.symphony -A -o yaml") + shell.Exec(ctx, "kubectl get solutions.solution.symphony -A -o yaml") + shell.Exec(ctx, "kubectl get instances.solution.symphony -A -o yaml") + shell.Exec(ctx, "helm list -A -o yaml") +} + +func CleanupManifests(ctx context.Context) error { + return shell.ExecAll( + ctx, + "kubectl delete instances.solution.symphony --all -A", + "kubectl delete targets.fabric.symphony --all -A", + "kubectl delete solutions.solution.symphony --all -A", + ) +} diff --git a/test/integration/lib/testhelpers/manifestbuilder.go b/test/integration/lib/testhelpers/manifestbuilder.go index dda66219e..25b6053d5 100644 --- a/test/integration/lib/testhelpers/manifestbuilder.go +++ b/test/integration/lib/testhelpers/manifestbuilder.go @@ -9,191 +9,10 @@ package testhelpers import ( "fmt" "os" + "regexp" "gopkg.in/yaml.v2" -) - -type ( - Metadata struct { - Annotations map[string]string `yaml:"annotations,omitempty"` - Name string `yaml:"name,omitempty"` - } - - // Solution describes the structure of symphony solution yaml file - Solution struct { - ApiVersion string `yaml:"apiVersion"` - Kind string `yaml:"kind"` - Metadata Metadata `yaml:"metadata"` - Spec SolutionSpec `yaml:"spec"` - } - - SolutionSpec struct { - DisplayName string `yaml:"displayName,omitempty"` - Metadata map[string]string `yaml:"metadata,omitempty"` - Components []ComponentSpec `yaml:"components,omitempty"` - } - - // Target describes the structure of symphony target yaml file - Target struct { - ApiVersion string `yaml:"apiVersion"` - Kind string `yaml:"kind"` - Metadata Metadata `yaml:"metadata"` - Spec TargetSpec `yaml:"spec"` - } - - TargetSpec struct { - DisplayName string `yaml:"displayName"` - Scope string `yaml:"scope,omitempty"` - Components []ComponentSpec `yaml:"components,omitempty"` - Topologies []Topology `yaml:"topologies"` - } - - Topology struct { - Bindings []Binding `yaml:"bindings"` - } - - Binding struct { - Config Config `yaml:"config"` - Provider string `yaml:"provider"` - Role string `yaml:"role"` - } - - Config struct { - InCluster string `yaml:"inCluster"` - } - - ComponentSpec struct { - Name string `yaml:"name"` - Properties map[string]interface{} `yaml:"properties"` - Type string `yaml:"type"` - } -) - -var ( - ComponetsMap = map[string]ComponentSpec{ - "e4k": { - Name: "e4k", - Properties: map[string]interface{}{ - "chart": map[string]interface{}{ - "repo": "e4kpreview.azurecr.io/helm/az-e4k", - "version": "0.3.0", - }, - }, - Type: "helm.v3", - }, - "e4k-broker": { - Name: "e4k-high-availability-broker", - Properties: map[string]interface{}{ - "chart": map[string]interface{}{ - "repo": "symphonycr.azurecr.io/az-e4k-broker", - "version": "0.1.0", - }, - }, - Type: "helm.v3", - }, - "bluefin-extension": { - Name: "bluefin", - Properties: map[string]interface{}{ - "chart": map[string]interface{}{ - "repo": "azbluefin.azurecr.io/helm/bluefin-arc-extension", - "version": "0.2.0-20230706.3-develop", - }, - }, - Type: "helm.v3", - }, - "bluefin-instance": { - Name: "bluefin-instance", - Properties: map[string]interface{}{ - "resource": map[string]interface{}{ - "apiVersion": "bluefin.az-bluefin.com/v1", - "kind": "Instance", - "metadata": map[string]interface{}{ - "name": "bf-instance", - "namespace": "default", - }, - "spec": map[string]interface{}{ - "displayName": "Test Instance", - "otelCollectorAddress": "otel-collector.alice-springs.svc.cluster.local:4317", - }, - }, - }, - Type: "yaml.k8s", - }, - - "bluefin-pipeline": { - Name: "test-pipeline", - Properties: map[string]interface{}{ - "resource": map[string]interface{}{ - "apiVersion": "bluefin.az-bluefin.com/v1", - "kind": "Pipeline", - "metadata": map[string]interface{}{ - "name": "bf-pipeline", - "namespace": "default", - }, - "spec": map[string]interface{}{ - "displayName": "bf-pipeline", - "enabled": true, - "input": map[string]interface{}{ - "description": "Read from topic Thermostat 3", - "displayName": "E4K", - "format": map[string]interface{}{"type": "json"}, - "mqttConnectionInfo": map[string]interface{}{ - "broker": "tcp://azedge-dmqtt-frontend:1883", - "password": "password", - "username": "client1", - }, - "next": []interface{}{"node-22f2"}, - "topics": []interface{}{ - map[string]interface{}{ - "name": "alice-springs/data/opc-ua-connector/opc-ua-connector/thermostat-sample-3", - }, - }, - "type": "input/mqtt@v1", - "viewOptions": map[string]interface{}{ - "position": map[string]interface{}{ - "x": 0, - "y": 80, - }, - }, - }, - "partitionCount": 6, - "stages": map[string]interface{}{ - "node-22f2": map[string]interface{}{ - "displayName": "No-op", - "next": []interface{}{"output"}, - "query": ".", - "type": "processor/transform@v1", - "viewOptions": map[string]interface{}{ - "position": map[string]interface{}{ - "x": 0, - "y": 208, - }, - }, - }, - "output": map[string]interface{}{ - "broker": "tcp://azedge-dmqtt-frontend:1883", - "description": "Publish to topic demo-output-topic", - "displayName": "E4K", - "format": map[string]interface{}{"type": "json"}, - "password": "password", - "timeout": "45ms", - "topic": "alice-springs/data/demo-output", - "type": "output/mqtt@v1", - "username": "client1", - "viewOptions": map[string]interface{}{ - "position": map[string]interface{}{ - "x": 0, - "y": 336, - }, - }, - }, - }, - }, - }, - }, - Type: "yaml.k8s", - }, - } + "k8s.io/apimachinery/pkg/util/uuid" ) // BuildManifestFile modifies the target/solution manifest files @@ -283,3 +102,159 @@ func addComponentsToTarget(data []byte, components []string) (Target, error) { return target, nil } + +type ( + InstanceOptions struct { + NamePostfix string + Scope string + Namespace string + Parameters map[string]interface{} + PostProcess func(*Instance) + Solution string + } + + SolutionOptions struct { + NamePostfix string + ComponentNames []string + Namespace string + PostProcess func(*Solution) + SolutionName string + } + + TargetOptions = struct { + NamePostfix string + Scope string + Namespace string + ComponentNames []string + Properties map[string]string + PostProcess func(*Target) + } +) + +const ( + AzureOperationIdKey = "management.azure.com/operationId" +) + +var leadingDash = regexp.MustCompile(`^-`) + +func PatchSolution(data []byte, opts SolutionOptions) ([]byte, error) { + var solution Solution + err := yaml.Unmarshal(data, &solution) + if err != nil { + return nil, err + } + yamlComponents := make([]ComponentSpec, 0) + for _, name := range opts.ComponentNames { + if val, ok := ComponetsMap[name]; ok { + yamlComponents = append(yamlComponents, val) + } else { + return nil, fmt.Errorf("component %s not found", name) + } + } + + if solution.Metadata.Annotations == nil { + solution.Metadata.Annotations = make(map[string]string) + } + + if opts.NamePostfix != "" { + solution.Metadata.Name = fmt.Sprintf("%s-%s", solution.Metadata.Name, opts.NamePostfix) + solution.Metadata.Name = leadingDash.ReplaceAllString(solution.Metadata.Name, "") + } + + if opts.Namespace != "" { + solution.Metadata.Namespace = opts.Namespace + } + + if opts.SolutionName != "" { + solution.Metadata.Name = opts.SolutionName + } + + solution.Metadata.Annotations[AzureOperationIdKey] = string(uuid.NewUUID()) + solution.Spec.Components = yamlComponents + if opts.PostProcess != nil { + opts.PostProcess(&solution) + } + return yaml.Marshal(solution) +} + +func PatchTarget(data []byte, opts TargetOptions) ([]byte, error) { + var target Target + err := yaml.Unmarshal(data, &target) + if err != nil { + return nil, err + } + + for _, name := range opts.ComponentNames { + if val, ok := ComponetsMap[name]; ok { + target.Spec.Components = append(target.Spec.Components, val) + } else { + return nil, fmt.Errorf("component %s not found", name) + } + } + if opts.NamePostfix != "" { + target.Metadata.Name = fmt.Sprintf("%s-%s", target.Metadata.Name, opts.NamePostfix) + target.Metadata.Name = leadingDash.ReplaceAllString(target.Metadata.Name, "") + } + + if opts.Namespace != "" { + target.Metadata.Namespace = opts.Namespace + } + + if opts.Scope != "" { + target.Spec.Scope = opts.Scope + } + + if target.Metadata.Annotations == nil { + target.Metadata.Annotations = make(map[string]string) + } + + if opts.Properties != nil { + target.Spec.Properties = opts.Properties + } + + target.Metadata.Annotations[AzureOperationIdKey] = string(uuid.NewUUID()) + if opts.PostProcess != nil { + opts.PostProcess(&target) + } + + return yaml.Marshal(target) +} + +func PatchInstance(data []byte, opts InstanceOptions) ([]byte, error) { + var instance Instance + err := yaml.Unmarshal(data, &instance) + if err != nil { + return nil, err + } + + if opts.NamePostfix != "" { + instance.Metadata.Name = fmt.Sprintf("%s-%s", instance.Metadata.Name, opts.NamePostfix) + instance.Metadata.Name = leadingDash.ReplaceAllString(instance.Metadata.Name, "") + } + + if opts.Namespace != "" { + instance.Metadata.Namespace = opts.Namespace + } + + if opts.Scope != "" { + instance.Spec.Scope = opts.Scope + } + + if opts.Solution != "" { + instance.Spec.Solution = opts.Solution + } + + if opts.Parameters != nil { + instance.Spec.Parameters = opts.Parameters + } + + if instance.Metadata.Annotations == nil { + instance.Metadata.Annotations = make(map[string]string) + } + + instance.Metadata.Annotations[AzureOperationIdKey] = string(uuid.NewUUID()) + if opts.PostProcess != nil { + opts.PostProcess(&instance) + } + return yaml.Marshal(instance) +} diff --git a/test/integration/lib/testhelpers/types.go b/test/integration/lib/testhelpers/types.go new file mode 100644 index 000000000..adfa0bbb2 --- /dev/null +++ b/test/integration/lib/testhelpers/types.go @@ -0,0 +1,89 @@ +package testhelpers + +// TODO: Switch over to symphony core types from the /k8s/api folder +type ( + Metadata struct { + Annotations map[string]string `yaml:"annotations,omitempty"` + Name string `yaml:"name,omitempty"` + Namespace string `yaml:"namespace,omitempty"` + } + + // Solution describes the structure of symphony solution yaml file + Solution struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata Metadata `yaml:"metadata"` + Spec SolutionSpec `yaml:"spec"` + } + + SolutionSpec struct { + DisplayName string `yaml:"displayName,omitempty"` + Scope string `yaml:"scope,omitempty"` + Metadata map[string]string `yaml:"metadata,omitempty"` + Components []ComponentSpec `yaml:"components,omitempty"` + } + + // Target describes the structure of symphony target yaml file + Target struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata Metadata `yaml:"metadata"` + Spec TargetSpec `yaml:"spec"` + } + + TargetSpec struct { + DisplayName string `yaml:"displayName"` + Scope string `yaml:"scope"` + Components []ComponentSpec `yaml:"components,omitempty"` + Topologies []Topology `yaml:"topologies"` + Properties map[string]string `yaml:"properties,omitempty"` + } + + Topology struct { + Bindings []Binding `yaml:"bindings"` + } + + Binding struct { + Config Config `yaml:"config"` + Provider string `yaml:"provider"` + Role string `yaml:"role"` + } + + Config struct { + InCluster string `yaml:"inCluster"` + } + + ComponentSpec struct { + Name string `yaml:"name"` + Parameters map[string]ParameterDefinition `yaml:"parameters,omitempty"` + Properties map[string]interface{} `yaml:"properties"` + Type string `yaml:"type"` + Constraints string `yaml:"constraints,omitempty"` + Dependencies []string `yaml:"dependencies,omitempty"` + } + + Instance struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata Metadata `yaml:"metadata"` + Spec InstanceSpec `yaml:"spec"` + } + + InstanceSpec struct { + DisplayName string `yaml:"displayName"` + Target TargetSelector `yaml:"target"` + Solution string `yaml:"solution"` + Scope string `yaml:"scope"` + Parameters map[string]interface{} `yaml:"parameters,omitempty"` + } + + TargetSelector struct { + Name string `yaml:"name,omitempty"` + Selector map[string]string `yaml:"selector,omitempty"` + } + + ParameterDefinition struct { + Type string `yaml:"type"` + DefaultValue interface{} `yaml:"default"` + } +) diff --git a/test/integration/magefile.go b/test/integration/magefile.go index 29eab13a3..0af83b8b1 100644 --- a/test/integration/magefile.go +++ b/test/integration/magefile.go @@ -93,7 +93,11 @@ func listTests(dir string) ([]string, error) { // Read test subfolders for _, entry := range subDirs { if entry.IsDir() { - results = append(results, filepath.Join(dir, entry.Name())) + dirPath := filepath.Join(dir, entry.Name()) + filePath := filepath.Join(dirPath, "magefile.go") + if _, err := os.Stat(filePath); err == nil { + results = append(results, dirPath) + } } } diff --git a/test/integration/scenarios/04.workflow/magefile.go b/test/integration/scenarios/04.workflow/magefile.go index 7add4985e..dfc8d8b71 100644 --- a/test/integration/scenarios/04.workflow/magefile.go +++ b/test/integration/scenarios/04.workflow/magefile.go @@ -22,7 +22,7 @@ import ( // Test config const ( TEST_NAME = "workflow test" - TEST_TIMEOUT = "3m" + TEST_TIMEOUT = "4m" ) var ( diff --git a/test/integration/scenarios/06.ado/create_update_fallback_test.go b/test/integration/scenarios/06.ado/create_update_fallback_test.go new file mode 100644 index 000000000..8b837822e --- /dev/null +++ b/test/integration/scenarios/06.ado/create_update_fallback_test.go @@ -0,0 +1,147 @@ +package scenarios_test + +import ( + "context" + _ "embed" + + "github.com/eclipse-symphony/symphony/packages/testutils/conditions" + "github.com/eclipse-symphony/symphony/packages/testutils/expectations" + "github.com/eclipse-symphony/symphony/packages/testutils/expectations/kube" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/eclipse-symphony/symphony/test/integration/lib/shell" + "github.com/eclipse-symphony/symphony/test/integration/lib/testhelpers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Create/update resources for rollback testing", Ordered, func() { + type TestCase struct { + TargetComponents []string + SolutionComponents []string + SolutionComponentsV2 []string + PostUpdateExpectation types.Expectation + PostRevertExpectation types.Expectation + TargetProperties map[string]string + } + var instanceBytes []byte + var targetBytes []byte + var solutionBytes []byte + var solutionBytesV2 []byte + var targetProps map[string]string + + BeforeAll(func(ctx context.Context) { + By("installing orchestrator in the cluster") + shell.LocalenvCmd(ctx, "mage cluster:deploy") + + By("setting the default testing lib logger") + logger.SetDefaultLogger(GinkgoWriter.Printf) + }) + + AfterAll(func() { + By("uninstalling orchestrator from the cluster") + err := shell.LocalenvCmd(context.Background(), "mage destroy all") + Expect(err).ToNot(HaveOccurred()) + }) + + JustAfterEach(func(ctx context.Context) { + if CurrentSpecReport().Failed() { + By("dumping cluster state") + testhelpers.DumpClusterState(ctx) + } + }) + + runner := func(ctx context.Context, testcase TestCase) { + By("setting the components for the target") + var err error + + props := targetProps + if testcase.TargetProperties != nil { + props = testcase.TargetProperties + } + // Patch the target manifest with the target options + targetBytes, err = testhelpers.PatchTarget(defaultTargetManifest, testhelpers.TargetOptions{ + ComponentNames: testcase.TargetComponents, + Properties: props, + }) + Expect(err).ToNot(HaveOccurred()) + + By("setting the components for Solution V1") + solutionBytes, err = testhelpers.PatchSolution(defaultSolutionManifest, testhelpers.SolutionOptions{ + ComponentNames: testcase.SolutionComponents, + }) + Expect(err).ToNot(HaveOccurred()) + + By("preparing the instance bytes with a new operation id for Solution V1") + instanceBytes, err = testhelpers.PatchInstance(defaultInstanceManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the Target") + err = shell.PipeInExec(ctx, "kubectl apply -f -", targetBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploying Solution V1") + err = shell.PipeInExec(ctx, "kubectl apply -f -", solutionBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the Instance that references Solution V1") + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceBytes) + Expect(err).ToNot(HaveOccurred()) + + By("setting the components for Solution V2, an invalid solution") + solutionBytesV2, err = testhelpers.PatchSolution(defaultSolutionManifest, testhelpers.SolutionOptions{ + ComponentNames: testcase.SolutionComponentsV2, + SolutionName: "solution-v2", + }) + Expect(err).ToNot(HaveOccurred()) + + By("deploying Solution V2") + err = shell.PipeInExec(ctx, "kubectl apply -f -", solutionBytesV2) + Expect(err).ToNot(HaveOccurred()) + + By("preparing the instance bytes with a new operation id for Solution V2") + instanceBytes, err = testhelpers.PatchInstance(defaultInstanceManifest, testhelpers.InstanceOptions{ + Solution: "solution-v2", + }) + Expect(err).ToNot(HaveOccurred()) + + By("updating the Instance to use Solution V2") + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceBytes) + Expect(err).ToNot(HaveOccurred()) + + By("verifying deployment of Instance referencing Solution V2 fails") + err = testcase.PostUpdateExpectation.Verify(ctx) + Expect(err).ToNot(HaveOccurred()) + + By("reverting the Instance to use Solution V1") + instanceBytes, err = testhelpers.PatchInstance(defaultInstanceManifest, testhelpers.InstanceOptions{ + Solution: "solution", + }) + Expect(err).ToNot(HaveOccurred()) + + By("Deploying the Instance to use Solution V1 again") + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceBytes) + Expect(err).ToNot(HaveOccurred()) + + By("verifying deployment of Instance referencing Solution V1 succeeds") + err = testcase.PostRevertExpectation.Verify(ctx) + Expect(err).ToNot(HaveOccurred()) + } + + DescribeTable("fail to deploy solution v2 then rollback to v1", Ordered, runner, + Entry("with a single component", TestCase{ + TargetComponents: []string{"simple-chart-1"}, + SolutionComponents: []string{"simple-chart-2"}, + SolutionComponentsV2: []string{"simple-chart-2-nonexistent"}, + PostUpdateExpectation: expectations.All( + kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( // make sure the instance named 'instance' is present in the 'default' namespace + kube.ProvisioningFailedCondition, // and it is failed + //jq.Equality(".status.provisioningStatus.error.details[0].details[0].code", "Update Failed"), + )))), + ), + PostRevertExpectation: expectations.All( + successfullInstanceExpectation, + ), + }), + ) +}) diff --git a/test/integration/scenarios/06.ado/create_update_test.go b/test/integration/scenarios/06.ado/create_update_test.go new file mode 100644 index 000000000..5bfcefa4b --- /dev/null +++ b/test/integration/scenarios/06.ado/create_update_test.go @@ -0,0 +1,540 @@ +package scenarios_test + +import ( + "context" + _ "embed" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/conditions" + "github.com/eclipse-symphony/symphony/packages/testutils/conditions/jq" + "github.com/eclipse-symphony/symphony/packages/testutils/expectations" + "github.com/eclipse-symphony/symphony/packages/testutils/expectations/helm" + "github.com/eclipse-symphony/symphony/packages/testutils/expectations/kube" + "github.com/eclipse-symphony/symphony/packages/testutils/helpers" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/eclipse-symphony/symphony/test/integration/lib/shell" + "github.com/eclipse-symphony/symphony/test/integration/lib/testhelpers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Create resources with sequential changes", Ordered, func() { + type TestCase struct { + TargetComponents []string + SolutionComponents []string + Expectation types.Expectation + TargetProperties map[string]string + InstanceParameters map[string]interface{} + } + var instanceBytes []byte + var targetBytes []byte + var solutionBytes []byte + var specTimeout = 120 * time.Second + var targetProps map[string]string + var instanceParams map[string]interface{} + + BeforeAll(func(ctx context.Context) { + By("installing orchestrator in the cluster") + shell.LocalenvCmd(ctx, "mage cluster:deploy") + + By("setting the default testing lib logger") + logger.SetDefaultLogger(GinkgoWriter.Printf) + }) + + AfterAll(func() { + By("uninstalling orchestrator from the cluster") + err := shell.LocalenvCmd(context.Background(), "mage destroy all") + Expect(err).ToNot(HaveOccurred()) + }) + + JustAfterEach(func(ctx context.Context) { + if CurrentSpecReport().Failed() { + By("dumping cluster state") + testhelpers.DumpClusterState(ctx) + } + }) + + runner := func(ctx context.Context, testcase TestCase) { + By("setting the components for the target") + var err error + props := targetProps + params := instanceParams + if testcase.TargetProperties != nil { + props = testcase.TargetProperties + } + + if testcase.InstanceParameters != nil { + params = testcase.InstanceParameters + } + targetBytes, err = testhelpers.PatchTarget(defaultTargetManifest, testhelpers.TargetOptions{ + ComponentNames: testcase.TargetComponents, + Properties: props, + }) + Expect(err).ToNot(HaveOccurred()) + + By("setting the components for the solution") + solutionBytes, err = testhelpers.PatchSolution(defaultSolutionManifest, testhelpers.SolutionOptions{ + ComponentNames: testcase.SolutionComponents, + }) + Expect(err).ToNot(HaveOccurred()) + + By("preparing the instance bytes with a new operation id for the test") + instanceBytes, err = testhelpers.PatchInstance(defaultInstanceManifest, testhelpers.InstanceOptions{ + Parameters: params, + }) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the instance") + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the target") + err = shell.PipeInExec(ctx, "kubectl apply -f -", targetBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the solution") + err = shell.PipeInExec(ctx, "kubectl apply -f -", solutionBytes) + Expect(err).ToNot(HaveOccurred()) + + err = testcase.Expectation.Verify(ctx) + Expect(err).ToNot(HaveOccurred()) + } + + DescribeTable("when performing create/update operations", Ordered, runner, + + Entry( + "it should deploy empty target and solution", SpecTimeout(specTimeout), + TestCase{ + Expectation: expectations.All( + successfullInstanceExpectation, + successfullTargetExpectation, + ), + }, + ), + + Entry( + "it should update the target with a simple helm chart", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-chart-1"}, + SolutionComponents: []string{}, + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-1", "Updated"), // and the target component 'simple-chart-1' status is updated. OSS has no provisioning status yet + )))), + successfullInstanceExpectation, + helm.MustNew("simple-chart-1", "azure-iot-operations", helm.WithReleaseCondition(helm.DeployedCondition)), // make sure the release is successfully deployed + ), + }, + ), + + Entry( + "it should deploy another simple helm chart in the solution so there are 2 helm releases", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-chart-1"}, // (same as previous entry) + SolutionComponents: []string{"simple-chart-2"}, + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-1", nil), // Because nothing changed, the output should be nil + )))), + kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( // make sure the instance named 'instance' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-2", "Updated"), // and the solution component 'simple-chart-2' is created + )))), + + helm.MustNew("simple-chart-.*", "azure-iot-operations", // releases beginning with 'simple-chart-' in the 'azure-iot-operations' namespace + helm.WithReleaseListCondition(conditions.Count(2)), // there should be only 2 releases present + helm.WithReleaseCondition(helm.DeployedCondition), // all releases should have 'deployed' status + ), + helm.MustNew("simple-chart-1", "azure-iot-operations"), + helm.MustNew("simple-chart-2", "azure-iot-operations"), + ), + }, + ), + + Entry( + "it should add a kubernetes config map in the solution", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-chart-1"}, // (same as previous entry) + SolutionComponents: []string{"simple-chart-2", "basic-configmap-1"}, // + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-1", nil), // Because the component didn't change + )))), + kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( // make sure the instance named 'instance' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-2", nil), // Because the component didn't change + //kube.ProvisioningStatusComponentOutput("target.basic-configmap-1", "Updated"), // and the solution component 'basic-configmap-1' is created + )))), + helm.MustNew("simple-chart-.*", "azure-iot-operations", // releases beginning with 'simple-chart-' in the 'azure-iot-operations' namespace + helm.WithReleaseListCondition(conditions.Count(2)), // there should be only 2 releases present + helm.WithReleaseCondition(helm.DeployedCondition), // all releases should have 'deployed' status + ), + kube.Must(kube.Resource("basic-configmap-1", "azure-iot-operations", helpers.ConfigMapGVK)), + ), + }, + ), + + Entry( + "it should add a kubernetes clusterrole in the target", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-chart-1", "basic-clusterrole"}, // + SolutionComponents: []string{"simple-chart-2", "basic-configmap-1"}, // (same as previous entry) + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-1", nil), // Because the component didn't change + //kube.ProvisioningStatusComponentOutput("target.basic-clusterrole", "Updated"), // and the target component 'basic-clusterrole' is created + )))), + kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( // make sure the instance named 'instance' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-2", nil), // Because the component didn't change + //kube.ProvisioningStatusComponentOutput("target.basic-configmap-1", nil), // Because the component didn't change + )))), + helm.MustNew("simple-chart-.*", "azure-iot-operations", // releases beginning with 'simple-chart-' in the 'azure-iot-operations' namespace + helm.WithReleaseListCondition(conditions.Count(2)), + helm.WithReleaseCondition(helm.DeployedCondition), + ), + kube.Must(kube.Resource("basic-clusterrole", "azure-iot-operations", helpers.ClusterRoleGVK)), + kube.Must(kube.Resource("basic-configmap-1", "azure-iot-operations", helpers.ConfigMapGVK)), + ), + }, + ), + + Entry( + "it should should just update the operation id when a no-op change is made", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-chart-1", "basic-clusterrole"}, // (same as previous entry) + SolutionComponents: []string{"simple-chart-2", "basic-configmap-1"}, // (same as previous entry) + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-1", nil), // Because the component didn't change + //kube.ProvisioningStatusComponentOutput("target.basic-clusterrole", nil), // Because the component didn't change + )))), + kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( // make sure the instance named 'instance' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-2", nil), // Because the component didn't change + //kube.ProvisioningStatusComponentOutput("target.basic-configmap-1", nil), // Because the component didn't change + )))), + helm.MustNew("simple-chart-.*", "azure-iot-operations", // releases beginning with 'simple-chart-' in the 'azure-iot-operations' namespace + helm.WithReleaseListCondition(conditions.Count(2)), // there should be only 2 releases present + helm.WithReleaseCondition(helm.DeployedCondition), // all releases should have 'deployed' status + ), + kube.Must(kube.Resource("basic-clusterrole", "azure-iot-operations", helpers.ClusterRoleGVK)), + kube.Must(kube.Resource("basic-configmap-1", "azure-iot-operations", helpers.GVK("", "v1", "ConfigMap"))), + ), + }, + ), + + Entry( + "It should update remove the clusterrole from the target", + TestCase{ + TargetComponents: []string{"simple-chart-1"}, + SolutionComponents: []string{"simple-chart-2", "basic-configmap-1"}, // (same as previous entry) + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + ////kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-1", nil), // Because the component didn't change + //kube.ProvisioningStatusComponentOutput("target.basic-clusterrole", "Deleted"), // and the target component 'basic-clusterrole' is deleted + )))), + kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( // make sure the instance named 'instance' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + //kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-2", nil), // Because the component didn't change + //kube.ProvisioningStatusComponentOutput("target.basic-configmap-1", nil), // Because the component didn't change + )))), + helm.MustNew("simple-chart-.*", "azure-iot-operations", // releases beginning with 'simple-chart-' in the 'azure-iot-operations' namespace + helm.WithReleaseListCondition(conditions.Count(2)), // there should be only 2 releases present + helm.WithReleaseCondition(helm.DeployedCondition), // all releases should have 'deployed' status + ), + kube.Must(kube.Resource("basic-configmap-1", "azure-iot-operations", helpers.GVK("", "v1", "ConfigMap"))), + kube.Must(kube.AbsentResource("basic-clusterrole", "azure-iot-operations", helpers.ClusterRoleGVK)), + ), + }, + ), + + Entry( + "It should update remove the simple-helmchart-2 from the solution", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-chart-1"}, // (same as previous entry) + SolutionComponents: []string{"basic-configmap-1"}, + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + //kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-1", nil), // Because the component didn't change + //kube.ProvisioningStatusComponentOutput("target.basic-clusterrole", nil), // Because it was deleted in the previous reconciliation + )))), + kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( // make sure the instance named 'instance' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + //kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-2", "Deleted"), // and the solution component 'simple-chart-2' is deleted + //kube.ProvisioningStatusComponentOutput("target.basic-configmap-1", nil), // Because the component didn't change + )))), + helm.MustNew("simple-chart-.*", "azure-iot-operations", // releases beginning with 'simple-chart-' in the 'azure-iot-operations' namespace + helm.WithReleaseListCondition(conditions.Count(1)), // make sure there is only 1 release left + helm.WithReleaseCondition(helm.DeployedCondition), // and it is deployed + ), + kube.Must(kube.Resource("basic-configmap-1", "azure-iot-operations", helpers.GVK("", "v1", "ConfigMap"))), // make sure the configmap still exists + helm.MustNew("simple-chart-1", "azure-iot-operations"), // make sure simple-chart-1 is still there + helm.MustNewAbsent("simple-chart-2", "azure-iot-operations"), // make sure simple-chart-2 is gone + ), + }, + ), + + Entry( + "It should update the simple-config-map-1 with new data", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-chart-1"}, // (same as previous entry) + SolutionComponents: []string{"basic-configmap-1-modified"}, // (same as previous entry but with new data) + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + //kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-chart-1", nil), // Because the component didn't change + )))), + kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( // make sure the instance named 'instance' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + //kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.basic-configmap-1", "Updated"), // and the solution component 'basic-configmap-1' is updated + )))), + helm.MustNew("simple-chart-.*", "azure-iot-operations", // releases beginning with 'simple-chart-' in the 'azure-iot-operations' namespace + helm.WithReleaseListCondition(conditions.Count(1)), // make sure there is only 1 release left + helm.WithReleaseCondition(helm.DeployedCondition), // and it is deployed + ), + kube.Must(kube.Resource("basic-configmap-1", "azure-iot-operations", // make sure the configmap still exists + helpers.GVK("", "v1", "ConfigMap"), + kube.WithCondition( + jq.Equality(".data.key", "value-modified"), // and the data is updated + ), + )), + helm.MustNew("simple-chart-1", "azure-iot-operations"), // make sure simple-chart-1 is still there + helm.MustNewAbsent("simple-chart-2", "azure-iot-operations"), // make sure simple-chart-2 is gone + ), + }, + ), + + Entry( + "it should fail the target when component is invalid", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-chart-1-nonexistent"}, // + SolutionComponents: []string{"basic-configmap-1-modified"}, // (same as previous entry) + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningFailedCondition, // and it is failed + //jq.Equality(".status.provisioningStatus.error.details[0].code", "Update Failed"), + //jq.Equality(".status.provisioningStatus.error.details[0].target", "simple-chart-1"), + )))), + successfullInstanceExpectation, + ), + }, + ), + + Entry( + "it should fail the solution when component is invalid", SpecTimeout(60*time.Second), + TestCase{ + TargetComponents: []string{"simple-chart-1-nonexistent"}, // (same as previous entry) + SolutionComponents: []string{"simple-chart-2-nonexistent"}, // + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningFailedCondition, // and it is failed + //jq.Equality(".status.provisioningStatus.error.details[0].code", "Update Failed"), + //jq.Equality(".status.provisioningStatus.error.details[0].target", "simple-chart-1"), + )))), + kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( // make sure the instance named 'instance' is present in the 'default' namespace + kube.ProvisioningFailedCondition, // and it is failed + //jq.Equality(".status.provisioningStatus.error.details[0].details[0].code", "Update Failed"), + //jq.Equality(".status.provisioningStatus.error.details[0].details[0].target", "simple-chart-2"), + )))), + )}, + ), + + Entry( + "it should update the target with a simple http", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-http"}, + SolutionComponents: []string{}, + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningSucceededCondition, // and it is successfully provisioned + //kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-http", "Updated"), // and the target component 'simple-http' status is updated + )))), + ), + }, + ), + + Entry( + "it should fail to update target with an invalid simple http", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-http-invalid-url"}, + SolutionComponents: []string{}, + Expectation: expectations.All( + kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( // make sure the target named 'target' is present in the 'default' namespace + kube.ProvisioningFailedCondition, // and it is failed + //kube.OperationIdMatchCondition, // and the status operation id matches the metadata operation id + //kube.ProvisioningStatusComponentOutput("target.simple-http", nil), // and the target component 'simple-http-invalid-url' status is failed to update + //jq.Equality(".status.provisioningStatus.error.details[0].target", "simple-http"), + //jq.Equality(".status.provisioningStatus.error.details[0].code", "Update Failed"), + )))), + ), + }, + ), + + // Marking as pending because even though this is correct, i.e, the order of component dependencies is respected, + // the status probe mechanism is broken. So because symphony cannot wait for the CRD to be in an "Established" state + // before deploying the CR, the CRD is not ready when the CR is deployed and the CR deployment fails. + PEntry( + "it should install the resources in the correct dependency order", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"simple-foobar", "foobar-crd"}, // explicitly orderering the CR before the CRD. This is not required but it is a good test + Expectation: expectations.All( + successfullInstanceExpectation, + successfullTargetExpectation, + kube.Must(kube.Resource("foobars.contoso.io", "azure-iot-operations", helpers.GVK("apiextensions.k8s.io", "v1", "CustomResourceDefinition"))), + kube.Must(kube.Resource("simple-foobar", "azure-iot-operations", helpers.GVK("contoso.io", "v1", "FooBar"))), + ), + }, + ), + ) + + Context("with component constraints", func() { + BeforeEach(func(ctx context.Context) { + By("setting the default target props") + targetProps = map[string]string{ + "OS": "windows", + } + }) + + AfterEach(func(ctx context.Context) { + By("resetting the default target props") + targetProps = nil + }) + + DescribeTable("when performing create/update operations", Ordered, runner, + Entry( + "should succeed when the component is deployed to a target with matching constraint", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"mongodb-constraint"}, + Expectation: successfullTargetExpectation, + }, + ), + Entry( + "should remove config map when the component is removed from the target", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"mongodb-constraint"}, + Expectation: successfullTargetExpectation, + }, + ), + Entry( + "should fail when the component constraint references nonexistent property", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"mongodb-constraint"}, + Expectation: failedTargetExpectation, + TargetProperties: map[string]string{ + "Arch": "arm", + }, + }, + ), + ) + }) + Context("with templated expressions", func() { + BeforeEach(func(ctx context.Context) { + By("setting the default target props") + targetProps = map[string]string{ + "OS": "windows", + "color": "blue", + } + }) + + AfterEach(func(ctx context.Context) { + By("resetting the default target props") + targetProps = nil + }) + + DescribeTable("when performing create/update operations with templated expressions", Ordered, runner, + Entry( + "it should succeed when the component is deployed to a target with existing properties", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"expressions-1"}, + Expectation: successfullTargetExpectation, + }, + ), + Entry( + "it should fail when the component is deployed to a target with non-existing properties", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"expressions-1-failed"}, + Expectation: failedTargetExpectation, + }, + ), + Entry( + "it should succeed when solution component has a valid expression", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"expressions-1"}, + SolutionComponents: []string{"expressions-1-soln"}, + Expectation: expectations.All( + successfullTargetExpectation, + successfullInstanceExpectation, + ), + }, + ), + Entry( + "it should fail solution component has an invalid expression", SpecTimeout(specTimeout), + TestCase{ + TargetComponents: []string{"expressions-1"}, + SolutionComponents: []string{"expressions-1-soln-failed"}, + Expectation: expectations.All( + successfullTargetExpectation, + failedInstanceExpectation, + ), + }, + ), + ) + }) + + Context("with instance parameters", func() { + BeforeEach(func(ctx context.Context) { + By("setting the default instance params") + instanceParams = map[string]interface{}{ + "database": "mongodb", + "database_uri": "mongodb://localhost:27017", + } + }) + + AfterEach(func(ctx context.Context) { + By("resetting the default instance params") + instanceParams = nil + }) + + DescribeTable("when performing create/update operations", Ordered, runner, + Entry( + "should succeed when the solution component is deployed to a target with the instance parameters", SpecTimeout(specTimeout), + TestCase{ + SolutionComponents: []string{"basic-configmap-1-params"}, + Expectation: successfullInstanceExpectation, + }, + ), + Entry( + "should fail when the solution component with missing parameter is deployed to a target", SpecTimeout(specTimeout), + TestCase{ + SolutionComponents: []string{"basic-configmap-1-params-modified"}, + Expectation: failedInstanceExpectation, + }, + ), + ) + }) +}) diff --git a/test/integration/scenarios/06.ado/delete_test.go b/test/integration/scenarios/06.ado/delete_test.go new file mode 100644 index 000000000..75c29300f --- /dev/null +++ b/test/integration/scenarios/06.ado/delete_test.go @@ -0,0 +1,175 @@ +package scenarios_test + +import ( + "context" + _ "embed" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/expectations" + "github.com/eclipse-symphony/symphony/packages/testutils/expectations/helm" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/eclipse-symphony/symphony/test/integration/lib/shell" + "github.com/eclipse-symphony/symphony/test/integration/lib/testhelpers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Delete", Ordered, func() { + var instanceBytes []byte + var targetBytes []byte + var solutionBytes []byte + var specTimeout = 2 * time.Minute + + type DeleteTestCase struct { + TargetComponents []string + SolutionComponents []string + PreDeleteExpectation types.Expectation + UnderlyingDeleteCommand string + OrcResourceToDelete *[]byte + PostDeleteExpectation types.Expectation + } + + BeforeAll(func(ctx context.Context) { + By("installing orchestrator in the cluster") + shell.LocalenvCmd(ctx, "mage cluster:deploy") + + By("setting the default testing lib logger") + logger.SetDefaultLogger(GinkgoWriter.Printf) + }) + + AfterAll(func() { + By("uninstalling orchestrator from the cluster") + err := shell.LocalenvCmd(context.Background(), "mage destroy all") + Expect(err).ToNot(HaveOccurred()) + }) + + JustAfterEach(func(ctx context.Context) { + if CurrentSpecReport().Failed() { + By("dumping cluster state") + testhelpers.DumpClusterState(ctx) + } + }) + + DescribeTable("when performing create/update operations", Ordered, + func(ctx context.Context, testcase DeleteTestCase) { + By("setting the components for the target") + var err error + targetBytes, err = testhelpers.PatchTarget(defaultTargetManifest, testhelpers.TargetOptions{ + ComponentNames: testcase.TargetComponents, + }) + Expect(err).ToNot(HaveOccurred()) + + By("setting the components for the solution") + solutionBytes, err = testhelpers.PatchSolution(defaultSolutionManifest, testhelpers.SolutionOptions{ + ComponentNames: testcase.SolutionComponents, + }) + Expect(err).ToNot(HaveOccurred()) + + By("preparing the instance bytes with a new operation id for the test") + instanceBytes, err = testhelpers.PatchInstance(defaultInstanceManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the instance") + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the target") + err = shell.PipeInExec(ctx, "kubectl apply -f -", targetBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the solution") + err = shell.PipeInExec(ctx, "kubectl apply -f -", solutionBytes) + Expect(err).ToNot(HaveOccurred()) + + By("verifying the resources before deletion") + err = testcase.PreDeleteExpectation.Verify(ctx) + Expect(err).ToNot(HaveOccurred()) + + By("deleting the underlying resources") + err = shell.Exec(ctx, testcase.UnderlyingDeleteCommand) + Expect(err).ToNot(HaveOccurred()) + + By("delete the orchestrator resource") + err = shell.PipeInExec(ctx, "kubectl delete -f -", *testcase.OrcResourceToDelete) + Expect(err).ToNot(HaveOccurred()) + + By("verifying the resources after deletion") + err = testcase.PostDeleteExpectation.Verify(ctx) + Expect(err).ToNot(HaveOccurred()) + }, + + Entry( + "it should delete the target when the underlying helm release is already gone", SpecTimeout(specTimeout), + DeleteTestCase{ + TargetComponents: []string{"simple-chart-1"}, + SolutionComponents: []string{}, + PreDeleteExpectation: expectations.All( + successfullInstanceExpectation, + successfullTargetExpectation, + helm.MustNew("simple-chart-1", "azure-iot-operations", helm.WithReleaseCondition(helm.DeployedCondition)), + ), + UnderlyingDeleteCommand: "helm uninstall simple-chart-1 -n azure-iot-operations --wait", + OrcResourceToDelete: &targetBytes, + PostDeleteExpectation: expectations.All( + successfullInstanceExpectation, + absentTargetExpectation, + ), + }, + ), + + Entry( + "it should delete the instance when the underlying helm release is already gone", SpecTimeout(specTimeout), + DeleteTestCase{ + TargetComponents: []string{}, + SolutionComponents: []string{"simple-chart-1"}, + PreDeleteExpectation: expectations.All( + successfullInstanceExpectation, + successfullTargetExpectation, + helm.MustNew("simple-chart-1", "azure-iot-operations", helm.WithReleaseCondition(helm.DeployedCondition)), + ), + UnderlyingDeleteCommand: "helm uninstall simple-chart-1 -n azure-iot-operations --wait", + OrcResourceToDelete: &instanceBytes, + PostDeleteExpectation: expectations.All( + absentInstanceExpectation, + successfullTargetExpectation, + ), + }, + ), + Entry( + "it should delete the target when the underlying kubernetes resource is already gone", SpecTimeout(specTimeout), + DeleteTestCase{ + TargetComponents: []string{"basic-configmap-1"}, + SolutionComponents: []string{}, + PreDeleteExpectation: expectations.All( + successfullInstanceExpectation, + successfullTargetExpectation, + ), + UnderlyingDeleteCommand: "kubectl delete configmap basic-configmap-1 -n azure-iot-operations", + OrcResourceToDelete: &targetBytes, + PostDeleteExpectation: expectations.All( + successfullInstanceExpectation, + absentTargetExpectation, + ), + }, + ), + + Entry( + "it should delete the instance when the underlying kubernetes resource is already gone", SpecTimeout(specTimeout), + DeleteTestCase{ + TargetComponents: []string{}, + SolutionComponents: []string{"basic-configmap-1"}, + PreDeleteExpectation: expectations.All( + successfullInstanceExpectation, + successfullTargetExpectation, + ), + UnderlyingDeleteCommand: "kubectl delete configmap basic-configmap-1 -n azure-iot-operations", + OrcResourceToDelete: &instanceBytes, + PostDeleteExpectation: expectations.All( + absentInstanceExpectation, + successfullTargetExpectation, + ), + }, + ), + ) +}) diff --git a/test/integration/scenarios/06.ado/manifest/instance.yaml b/test/integration/scenarios/06.ado/manifest/instance.yaml new file mode 100644 index 000000000..3e82335dd --- /dev/null +++ b/test/integration/scenarios/06.ado/manifest/instance.yaml @@ -0,0 +1,9 @@ +apiVersion: solution.symphony/v1 +kind: Instance +metadata: + name: instance +spec: + target: + name: target + solution: solution + scope: azure-iot-operations diff --git a/test/integration/scenarios/06.ado/manifest/solution.yaml b/test/integration/scenarios/06.ado/manifest/solution.yaml new file mode 100644 index 000000000..c78e09cae --- /dev/null +++ b/test/integration/scenarios/06.ado/manifest/solution.yaml @@ -0,0 +1,6 @@ +apiVersion: solution.symphony/v1 +kind: Solution +metadata: + name: solution +spec: + components: [] \ No newline at end of file diff --git a/test/integration/scenarios/06.ado/manifest/target.yaml b/test/integration/scenarios/06.ado/manifest/target.yaml new file mode 100644 index 000000000..2b36d6f17 --- /dev/null +++ b/test/integration/scenarios/06.ado/manifest/target.yaml @@ -0,0 +1,25 @@ +apiVersion: fabric.symphony/v1 +kind: Target +metadata: + name: target +spec: + scope: azure-iot-operations + components: [] + topologies: + - bindings: + - config: + inCluster: "true" + provider: providers.target.k8s + role: instance + - config: + inCluster: "true" + provider: providers.target.helm + role: helm.v3 + - config: + inCluster: "true" + provider: providers.target.kubectl + role: yaml.k8s + - config: + inCluster: "true" + provider: providers.target.http + role: http diff --git a/test/integration/scenarios/06.ado/rbac_test.go b/test/integration/scenarios/06.ado/rbac_test.go new file mode 100644 index 000000000..2ea806f81 --- /dev/null +++ b/test/integration/scenarios/06.ado/rbac_test.go @@ -0,0 +1,220 @@ +package scenarios_test + +import ( + "context" + _ "embed" + "fmt" + "time" + + "github.com/eclipse-symphony/symphony/packages/testutils/expectations" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/packages/testutils/types" + "github.com/eclipse-symphony/symphony/test/integration/lib/shell" + "github.com/eclipse-symphony/symphony/test/integration/lib/testhelpers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("RBAC", Ordered, func() { + type Rbac struct { + TargetComponents []string + SolutionComponents []string + InstanceScope string + TargetScope string + Expectation types.Expectation + } + type HelmValues = testhelpers.HelmValues + type Array = testhelpers.Array + type TArray[T any] []T + + var instanceBytes []byte + var targetBytes []byte + var solutionBytes []byte + var specTimeout = 3 * time.Minute + var installValues HelmValues + var runRbacTest = func(ctx context.Context, testcase Rbac) { + By("setting the components for the target and scope") + var err error + targetBytes, err = testhelpers.PatchTarget(defaultTargetManifest, testhelpers.TargetOptions{ + ComponentNames: testcase.TargetComponents, + Scope: testcase.TargetScope, + }) + Expect(err).ToNot(HaveOccurred()) + + By("setting the components for the solution") + solutionBytes, err = testhelpers.PatchSolution(defaultSolutionManifest, testhelpers.SolutionOptions{ + ComponentNames: testcase.SolutionComponents, + }) + Expect(err).ToNot(HaveOccurred()) + + By("setting the instance scope") + instanceBytes, err = testhelpers.PatchInstance(defaultInstanceManifest, testhelpers.InstanceOptions{ + Scope: testcase.InstanceScope, + }) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the instance") + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the target") + err = shell.PipeInExec(ctx, "kubectl apply -f -", targetBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploying the solution") + err = shell.PipeInExec(ctx, "kubectl apply -f -", solutionBytes) + Expect(err).ToNot(HaveOccurred()) + + By("verifying the resources") + err = testcase.Expectation.Verify(ctx) + Expect(err).ToNot(HaveOccurred()) + } + + BeforeAll(func(ctx context.Context) { + By("setting the default testing lib logger") + logger.SetDefaultLogger(GinkgoWriter.Printf) + }) + + AfterAll(func() { + By("uninstalling orchestrator from the cluster") + err := shell.LocalenvCmd(context.Background(), "mage destroy all") + Expect(err).ToNot(HaveOccurred()) + }) + + JustAfterEach(func(ctx context.Context) { + if CurrentSpecReport().Failed() { + By("dumping cluster state") + testhelpers.DumpClusterState(ctx) + } + }) + + When("orchestrator is installed as cluster admin", func() { + BeforeAll(func(ctx context.Context) { + By("setting the install values") + installValues = testhelpers.HelmValues{ + "rbac": testhelpers.HelmValues{ + "cluster": testhelpers.HelmValues{ + "admin": true, // Grant symphony cluster admin + }, + }, + } + + By("installing orchestrator in the cluster") + err := shell.LocalenvCmd(ctx, fmt.Sprintf("mage cluster:deploywithsettings '%s'", installValues.String())) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterAll(func(ctx context.Context) { + By("removing all instances, targets and solutions from the cluster") + err := testhelpers.CleanupManifests(ctx) + Expect(err).ToNot(HaveOccurred()) + }) + + DescribeTable("when performing CRUD operations", Ordered, runRbacTest, + Entry( + "it succefully install in default namespace", SpecTimeout(specTimeout), + Rbac{ + TargetComponents: []string{"basic-clusterrole"}, + SolutionComponents: []string{"simple-chart-1"}, + Expectation: expectations.All( + successfullInstanceExpectation, + successfullInstanceExpectation, + ), + }, + ), + ) + }) + + When("orchestrator is installed as namespace admin", func() { + BeforeAll(func(ctx context.Context) { + By("setting the install values") + installValues = HelmValues{ + "rbac": HelmValues{ + "cluster": HelmValues{ + "admin": false, // Deny symphony cluster admin + }, + "namespace": HelmValues{ + "releaseNamespaceAdmin": true, // Grant symphony namespace admin (default namespace) + }, + }, + } + + By("installing orchestrator in the cluster") + err := shell.LocalenvCmd(ctx, fmt.Sprintf("mage cluster:deploywithsettings '%s'", installValues.String())) + Expect(err).ToNot(HaveOccurred()) + + }) + + AfterAll(func(ctx context.Context) { + By("removing all instances, targets and solutions from the cluster") + err := testhelpers.CleanupManifests(ctx) + Expect(err).ToNot(HaveOccurred()) + }) + + DescribeTable("when performing CRUD operations", Ordered, runRbacTest, + Entry( + "it succefully install in default namespace", SpecTimeout(specTimeout), + Rbac{ + TargetComponents: []string{"mongodb-configmap"}, // Namespace level resource (configmap) + SolutionComponents: []string{"basic-configmap-1"}, // Namespace level resource (configmap) + TargetScope: "default", // Places the target component in the same namesapce as orchestrator + InstanceScope: "default", // Places the solution component in the same namesapce as orchestrator + Expectation: expectations.All( + successfullInstanceExpectation, + successfullInstanceExpectation, + ), + }, + ), + ) + }) + + When("orchestrator is installed with specific namespace rules", func() { + BeforeAll(func(ctx context.Context) { + By("setting the install values") + installValues = HelmValues{ + "rbac": HelmValues{ + "cluster": HelmValues{ + "admin": false, // Deny symphony cluster admin + }, + "namespace": HelmValues{ + "namespaces": HelmValues{ + "namespace-a": HelmValues{ + "rules": TArray[HelmValues]{{ + "resources": Array{"configmaps"}, + "verbs": Array{"*"}, + "apiGroups": Array{""}, + }}, + }, + }, + }, + }, + } + + By("installing orchestrator in the cluster") + err := shell.LocalenvCmd(ctx, fmt.Sprintf("mage cluster:deploywithsettings '%s'", installValues.String())) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterAll(func(ctx context.Context) { + By("removing all instances, targets and solutions from the cluster") + err := testhelpers.CleanupManifests(ctx) + Expect(err).ToNot(HaveOccurred()) + }) + + DescribeTable("when performing CRUD operations", Ordered, runRbacTest, + Entry( + "it succefully install in default namespace", SpecTimeout(specTimeout), + Rbac{ + TargetComponents: []string{"mongodb-configmap"}, // Namespace level resource (configmap) + SolutionComponents: []string{"basic-configmap-1"}, // Namespace level resource (configmap) + InstanceScope: "namespace-a", // Places the solution component in the allowed namespace + TargetScope: "namespace-a", // Places the target component in the allowed namespace + Expectation: expectations.All( + successfullInstanceExpectation, + successfullInstanceExpectation, + ), + }, + ), + ) + }) +}) diff --git a/test/integration/scenarios/06.ado/suite_test.go b/test/integration/scenarios/06.ado/suite_test.go new file mode 100644 index 000000000..27767e959 --- /dev/null +++ b/test/integration/scenarios/06.ado/suite_test.go @@ -0,0 +1,62 @@ +package scenarios_test + +import ( + "context" + "testing" + "time" + + _ "embed" + + "github.com/eclipse-symphony/symphony/packages/testutils/conditions" + "github.com/eclipse-symphony/symphony/packages/testutils/expectations/kube" + "github.com/eclipse-symphony/symphony/packages/testutils/logger" + "github.com/eclipse-symphony/symphony/test/integration/lib/shell" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +//go:embed manifest/instance.yaml +var defaultInstanceManifest []byte + +//go:embed manifest/target.yaml +var defaultTargetManifest []byte + +//go:embed manifest/solution.yaml +var defaultSolutionManifest []byte + +var successfullTargetExpectation = kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( + kube.ProvisioningSucceededCondition, + //kube.OperationIdMatchCondition, +)))) + +var successfullInstanceExpectation = kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( + kube.ProvisioningSucceededCondition, + //kube.OperationIdMatchCondition, +)))) + +var failedTargetExpectation = kube.Must(kube.Target("target", "default", kube.WithCondition(conditions.All( + kube.ProvisioningFailedCondition, + //kube.OperationIdMatchCondition, +)))) + +var failedInstanceExpectation = kube.Must(kube.Instance("instance", "default", kube.WithCondition(conditions.All( + kube.ProvisioningFailedCondition, + //kube.OperationIdMatchCondition, +)))) + +var absentInstanceExpectation = kube.Must(kube.AbsentInstance("instance", "default")) +var absentTargetExpectation = kube.Must(kube.AbsentTarget("target", "default")) + +var _ = BeforeSuite(func(ctx context.Context) { + // err := shell.LocalenvCmd(ctx, "mage cluster:load") + // Expect(err).ToNot(HaveOccurred()) + + shell.LocalenvCmd(ctx, "mage cluster:deploy") + + logger.SetDefaultLogger(GinkgoWriter.Printf) +}, NodeTimeout(5*time.Minute)) + +func TestScenarios(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Scenarios Suite") +} diff --git a/test/localenv/magefile.go b/test/localenv/magefile.go index 5ec3cd6c6..c85e739e5 100644 --- a/test/localenv/magefile.go +++ b/test/localenv/magefile.go @@ -52,8 +52,30 @@ type License mg.Namespace // Deploys the symphony ecosystem to your local Minikube cluster. func (Cluster) Deploy() error { fmt.Printf("Deploying symphony to minikube\n") - helmUpgrade := fmt.Sprintf("helm upgrade %s %s --install -n %s --create-namespace --wait -f ../../packages/helm/symphony/values.yaml -f symphony-ghcr-values.yaml --set symphonyImage.tag=%s --set paiImage.tag=%s", RELEASE_NAME, CHART_PATH, NAMESPACE, DOCKER_TAG, DOCKER_TAG) - return shellcmd.Command(helmUpgrade).Run() + mg.Deps(ensureMinikubeUp) + certsToVerify := []string{"symphony-api-serving-cert ", "symphony-serving-cert"} + commands := []shellcmd.Command{ + shellcmd.Command(fmt.Sprintf("helm upgrade %s %s --install -n %s --create-namespace --wait -f ../../packages/helm/symphony/values.yaml -f symphony-ghcr-values.yaml --set symphonyImage.tag=%s --set paiImage.tag=%s", RELEASE_NAME, CHART_PATH, NAMESPACE, DOCKER_TAG, DOCKER_TAG)), + } + for _, cert := range certsToVerify { + commands = append(commands, shellcmd.Command(fmt.Sprintf("kubectl wait --for=condition=ready certificates %s -n %s --timeout=90s", cert, NAMESPACE))) + } + return shellcmd.RunAll(commands...) +} + +// Deploys the symphony ecosystem to your local Minikube cluster with the provided settings. Note that this would also deploy cert-manager separately. +// E.g. mage deployWithSettings '--set some.key=some_value --set another.key=another_value' +func (Cluster) DeployWithSettings(values string) error { + fmt.Printf("Deploying symphony to minikube with settings, %s\n", values) + mg.Deps(ensureMinikubeUp) + certsToVerify := []string{"symphony-api-serving-cert ", "symphony-serving-cert"} + commands := []shellcmd.Command{ + shellcmd.Command(fmt.Sprintf("helm upgrade %s %s --install -n %s --create-namespace --wait -f ../../packages/helm/symphony/values.yaml -f symphony-ghcr-values.yaml --set symphonyImage.tag=latest --set paiImage.tag=latest %s", RELEASE_NAME, CHART_PATH, NAMESPACE, values)), + } + for _, cert := range certsToVerify { + commands = append(commands, shellcmd.Command(fmt.Sprintf("kubectl wait --for=condition=ready certificates %s -n %s --timeout=90s", cert, NAMESPACE))) + } + return shellcmd.RunAll(commands...) } // Up brings the minikube cluster up with symphony deployed From 3cb5392e7f2f8989795b5d144ad0dd9060974cbc Mon Sep 17 00:00:00 2001 From: Jiawei Du <59427055+msftcoderdjw@users.noreply.github.com> Date: Wed, 8 May 2024 07:58:13 +0800 Subject: [PATCH 13/26] Use BOT_USER (eclipse-symphoy-bot) to checkout, commit, push changes in release pipeline (#247) * Fix checkout/commit/push users * address comments --- .github/workflows/release.yaml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 05f08002f..58d2434b5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,11 +2,19 @@ name: Release on: workflow_dispatch: -permissions: write-all +permissions: + contents: write + packages: write env: ContainerRegistry: "ghcr.io" ContainerRegistryRepo: "ghcr.io/eclipse-symphony" + BOT_USER_NAME: eclipse-symphoy-bot + BOT_EMAIL_ID: symphony-bot@eclipse.org + IMAGE_NAME: ${{ github.repository }} +# Two users are used in this pipeline +# BOT_USER_NAME (eclipse-symphoy-bot) / secrets.BOT_GITHUB_TOKEN is used to checkout/commit/push the changes to the repository +# github.repository_owner / secrets.GITHUB_TOKEN is used to login to the docker registry and helm registry and to create the release jobs: build: if: github.repository == 'eclipse-symphony/symphony' && (github.actor == 'chgennar' || github.actor == 'juancooldude' || github.actor == 'Haishi2016' || github.actor == 'nonsocode' || github.actor == 'msftcoderdjw' || github.actor == 'TonyXiaofeng' || github.actor == 'iwangjintian') @@ -14,6 +22,13 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + token: ${{ secrets.BOT_GITHUB_TOKEN }} + + - name: Git config + run: | + git config user.name ${{ env.BOT_USER_NAME }} + git config user.email ${{ env.BOT_EMAIL_ID }} - name: Install dependencies run: | @@ -150,8 +165,8 @@ jobs: - name: Push changes uses: ad-m/github-push-action@master with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: experimental + github_token: ${{ secrets.BOT_GITHUB_TOKEN }} + branch: main - name: Create Release id: create_release From 952f021fc8c75e0812e8cb34f6b9a5d96046a64c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 8 May 2024 00:11:07 +0000 Subject: [PATCH 14/26] Bump version to 0.48.23 --- .github/version/versions.txt | 2 +- cli/cmd/up.go | 2 +- packages/helm/symphony/Chart.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/version/versions.txt b/.github/version/versions.txt index 4ca04cee4..ddc1aee2c 100644 --- a/.github/version/versions.txt +++ b/.github/version/versions.txt @@ -1 +1 @@ -0.48-exp.21 \ No newline at end of file +0.48.23 \ No newline at end of file diff --git a/cli/cmd/up.go b/cli/cmd/up.go index 4313e6b0e..9bf0905c5 100644 --- a/cli/cmd/up.go +++ b/cli/cmd/up.go @@ -22,7 +22,7 @@ import ( ) // The version is auto updated by the release pipeline, do not change it manually -const SymphonyAPIVersion = "0.48.22" +const SymphonyAPIVersion = "0.48.23" const KANPortalVersion = "0.39.0-main-603f4b9-amd64" var ( diff --git a/packages/helm/symphony/Chart.yaml b/packages/helm/symphony/Chart.yaml index 6bf7c3381..70e7776eb 100644 --- a/packages/helm/symphony/Chart.yaml +++ b/packages/helm/symphony/Chart.yaml @@ -3,9 +3,9 @@ name: symphony description: A Helm chart for Symphony control plane type: application # The version is auto updated by the release pipeline, do not change it manually -version: "0.48.22" +version: "0.48.23" # The version is auto updated by the release pipeline, do not change it manually -appVersion: "0.48.22" +appVersion: "0.48.23" dependencies: - name: cert-manager version: "1.13.1" From 5d414bd19456e344561794c7eee0c86e2efcf8d3 Mon Sep 17 00:00:00 2001 From: Xingdong Li Date: Wed, 8 May 2024 12:12:29 +0800 Subject: [PATCH 15/26] Remove CatalogSpec.SiteId (#236) --- agent/src/models.rs | 2 - .../catalogs/catalogs-manager_test.go | 3 +- .../managers/staging/staging-manager_test.go | 1 - .../managers/sync/sync-manager_test.go | 3 +- api/pkg/apis/v1alpha1/model/catalog.go | 5 --- api/pkg/apis/v1alpha1/model/catalog_test.go | 20 ++-------- .../providers/target/staging/staging.go | 1 - .../v1alpha1/vendors/catalogs-vendor_test.go | 6 +-- .../vendors/federation-vendor_test.go | 6 +-- .../v1alpha1/vendors/visualization-vendor.go | 3 +- .../vendors/visualization-vendor_test.go | 6 --- .../approval/logicapp/instance-catalog.yaml | 3 +- .../approval/script/instance-catalog.yaml | 3 +- docs/samples/multisite/catalog-catalog.yaml | 3 +- docs/samples/multisite/instance-catalog.yaml | 3 +- docs/samples/multisite/solution-catalog.yaml | 3 +- docs/samples/multisite/target-catalog.yaml | 3 +- docs/samples/opera/app/types.d.ts | 3 +- .../components/editors/CatalogEditor.tsx | 1 - .../chemical-factory-2/catalogs/assets.yaml | 27 +++++-------- .../catalogs/configurations.yaml | 21 ++++------ .../configurations-with-schema.yaml | 3 +- .../chemical-factory/configurations.yaml | 33 ++++++---------- .../chemical-factory/manifests.yaml | 18 +++------ .../chemical-factory/schemas.yaml | 3 +- .../universe-data/chemical-factory/sites.yaml | 39 +++++++------------ .../api/symphony-api-openapi.yaml | 4 -- .../concepts/unified-object-model/catalog.md | 3 -- .../define-configurations.md | 3 +- k8s/apis/model/v1/common_types.go | 1 - .../bases/federation.symphony_catalogs.yaml | 3 -- .../templates/symphony-core/symphonyk8s.yaml | 3 -- .../manifest/instance-catalog.yaml | 3 +- .../scenarios/05.catalog/catalogs/asset.yaml | 3 +- .../scenarios/05.catalog/catalogs/config.yaml | 3 +- .../05.catalog/catalogs/instance.yaml | 3 +- .../scenarios/05.catalog/catalogs/schema.yaml | 3 +- .../05.catalog/catalogs/solution.yaml | 3 +- .../scenarios/05.catalog/catalogs/target.yaml | 3 +- .../05.catalog/catalogs/wrongconfig.yaml | 3 +- 40 files changed, 74 insertions(+), 189 deletions(-) diff --git a/agent/src/models.rs b/agent/src/models.rs index 2e313edfc..1f16a3060 100644 --- a/agent/src/models.rs +++ b/agent/src/models.rs @@ -42,8 +42,6 @@ pub struct StagedProperties { } #[derive(Serialize, Deserialize)] pub struct CatalogSpec { - #[serde(rename = "siteId")] - site_id: String, #[serde(rename = "type")] pub catalog_type: String, pub properties: StagedProperties, diff --git a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go index a54bda182..7d769f884 100644 --- a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager_test.go @@ -32,8 +32,7 @@ var catalogState = model.CatalogState{ Name: "name1", }, Spec: &model.CatalogSpec{ - SiteId: "site1", - Type: "catalog", + Type: "catalog", Properties: map[string]interface{}{ "property1": "value1", "property2": "value2", diff --git a/api/pkg/apis/v1alpha1/managers/staging/staging-manager_test.go b/api/pkg/apis/v1alpha1/managers/staging/staging-manager_test.go index f8864c4bd..ce0314022 100644 --- a/api/pkg/apis/v1alpha1/managers/staging/staging-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/staging/staging-manager_test.go @@ -155,7 +155,6 @@ func InitializeMockSymphonyAPI() *httptest.Server { Name: "catalog1", }, Spec: &model.CatalogSpec{ - SiteId: "fake", Generation: "1", ParentName: "fakeparent", }, diff --git a/api/pkg/apis/v1alpha1/managers/sync/sync-manager_test.go b/api/pkg/apis/v1alpha1/managers/sync/sync-manager_test.go index 61b915e7d..69ff2b17d 100644 --- a/api/pkg/apis/v1alpha1/managers/sync/sync-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/sync/sync-manager_test.go @@ -115,8 +115,7 @@ func InitiazlizeMockSymphonyAPI(siteId string) *httptest.Server { Name: "catalog1", }, Spec: &model.CatalogSpec{ - SiteId: "parent", - Type: "Instance", + Type: "Instance", Properties: map[string]interface{}{ "foo": "bar", }, diff --git a/api/pkg/apis/v1alpha1/model/catalog.go b/api/pkg/apis/v1alpha1/model/catalog.go index a311e0377..7a912f779 100644 --- a/api/pkg/apis/v1alpha1/model/catalog.go +++ b/api/pkg/apis/v1alpha1/model/catalog.go @@ -31,7 +31,6 @@ type ObjectRef struct { Metadata map[string]string `json:"metadata,omitempty"` } type CatalogSpec struct { - SiteId string `json:"siteId"` Type string `json:"type"` Metadata map[string]string `json:"metadata,omitempty"` Properties map[string]interface{} `json:"properties"` @@ -50,10 +49,6 @@ func (c CatalogSpec) DeepEquals(other IDeepEquals) (bool, error) { return false, errors.New("parameter is not a CatalogSpec type") } - if c.SiteId != otherC.SiteId { - return false, nil - } - if c.ParentName != otherC.ParentName { return false, nil } diff --git a/api/pkg/apis/v1alpha1/model/catalog_test.go b/api/pkg/apis/v1alpha1/model/catalog_test.go index 50bb0d81e..097807ed4 100644 --- a/api/pkg/apis/v1alpha1/model/catalog_test.go +++ b/api/pkg/apis/v1alpha1/model/catalog_test.go @@ -30,7 +30,6 @@ func TestIntefaceConvertion(t *testing.T) { } func TestCatalogMatch(t *testing.T) { catalog1 := CatalogSpec{ - SiteId: "siteId", ParentName: "parentName", Generation: "1", Properties: map[string]interface{}{ @@ -38,7 +37,6 @@ func TestCatalogMatch(t *testing.T) { }, } catalog2 := CatalogSpec{ - SiteId: "siteId", ParentName: "parentName", Generation: "1", Properties: map[string]interface{}{ @@ -63,25 +61,13 @@ func TestCatalogMatchOneEmpty(t *testing.T) { } func TestCatalogNotMatch(t *testing.T) { - catalog1 := CatalogSpec{ - SiteId: "siteId", - } - catalog2 := CatalogSpec{ - SiteId: "siteId2", - } - - // siteId not match - catalog1.SiteId = "siteId" - catalog2.SiteId = "siteId2" - equal, err := catalog1.DeepEquals(catalog2) - assert.Nil(t, err) - assert.False(t, equal) + catalog1 := CatalogSpec{} + catalog2 := CatalogSpec{} // parentName not match - catalog2.SiteId = "siteId" catalog1.ParentName = "parentName" catalog2.ParentName = "parentName2" - equal, err = catalog1.DeepEquals(catalog2) + equal, err := catalog1.DeepEquals(catalog2) assert.Nil(t, err) assert.False(t, equal) diff --git a/api/pkg/apis/v1alpha1/providers/target/staging/staging.go b/api/pkg/apis/v1alpha1/providers/target/staging/staging.go index f6d2e6467..a3c848514 100644 --- a/api/pkg/apis/v1alpha1/providers/target/staging/staging.go +++ b/api/pkg/apis/v1alpha1/providers/target/staging/staging.go @@ -178,7 +178,6 @@ func (i *StagingTargetProvider) Apply(ctx context.Context, deployment model.Depl if catalog.Spec == nil { catalog.ObjectMeta.Name = deployment.Instance.ObjectMeta.Name + "-" + i.Config.TargetName catalog.Spec = &model.CatalogSpec{ - SiteId: i.Context.SiteInfo.SiteId, Type: "staged", } } diff --git a/api/pkg/apis/v1alpha1/vendors/catalogs-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/catalogs-vendor_test.go index 22e5e806a..84852b593 100644 --- a/api/pkg/apis/v1alpha1/vendors/catalogs-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/catalogs-vendor_test.go @@ -28,8 +28,7 @@ var catalogState = model.CatalogState{ Name: "name1", }, Spec: &model.CatalogSpec{ - SiteId: "site1", - Type: "catalog", + Type: "catalog", Properties: map[string]interface{}{ "property1": "value1", "property2": "value2", @@ -174,8 +173,7 @@ func TestCatalogOnCheck(t *testing.T) { var catalogState = model.CatalogState{ Spec: &model.CatalogSpec{ - SiteId: "site1", - Type: "catalog", + Type: "catalog", Properties: map[string]interface{}{ "property1": "value1", "property2": "value2", diff --git a/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go index a8faf1949..f80558b26 100644 --- a/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go @@ -374,8 +374,7 @@ func TestFederationOnSyncGet(t *testing.T) { Name: "catalog1", }, Spec: &model.CatalogSpec{ - SiteId: vendor.Config.SiteInfo.SiteId, - Type: "catalog", + Type: "catalog", Properties: map[string]interface{}{ "property1": "value1", "property2": "value2", @@ -470,8 +469,7 @@ func TestFederationOnK8SHook(t *testing.T) { Name: "catalog1", }, Spec: &model.CatalogSpec{ - SiteId: vendor.Config.SiteInfo.SiteId, - Type: "catalog", + Type: "catalog", Properties: map[string]interface{}{ "property1": "value1", "property2": "value2", diff --git a/api/pkg/apis/v1alpha1/vendors/visualization-vendor.go b/api/pkg/apis/v1alpha1/vendors/visualization-vendor.go index b10bf5f59..b3624b652 100644 --- a/api/pkg/apis/v1alpha1/vendors/visualization-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/visualization-vendor.go @@ -199,8 +199,7 @@ func mergeCatalogs(existingCatalog, newCatalog model.CatalogState) (model.Catalo func convertVisualizationPacketToCatalog(site string, packet model.Packet) (model.CatalogState, error) { catalog := model.CatalogState{ Spec: &model.CatalogSpec{ - SiteId: site, - Type: "topology", + Type: "topology", Properties: map[string]interface{}{ packet.From: map[string]model.Packet{ packet.To: packet, diff --git a/api/pkg/apis/v1alpha1/vendors/visualization-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/visualization-vendor_test.go index 75f93d971..76984ee59 100644 --- a/api/pkg/apis/v1alpha1/vendors/visualization-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/visualization-vendor_test.go @@ -112,7 +112,6 @@ func TestConvertVisualizationPacketToCatalog(t *testing.T) { DataType: "bytes", }) assert.Nil(t, err) - assert.Equal(t, "fake-site", catalog.Spec.SiteId) assert.Equal(t, "topology", catalog.Spec.Type) v, ok := catalog.Spec.Properties["from-1"].(map[string]model.Packet) @@ -131,7 +130,6 @@ func TestConvertVisualizationPacketToCatalogNoData(t *testing.T) { To: "to-1", }) assert.Nil(t, err) - assert.Equal(t, "fake-site", catalog.Spec.SiteId) assert.Equal(t, "topology", catalog.Spec.Type) v, ok := catalog.Spec.Properties["from-1"].(map[string]model.Packet) @@ -150,7 +148,6 @@ func TestMergeCatalogsSameKey(t *testing.T) { DataType: "bytes", }) assert.Nil(t, err) - assert.Equal(t, "fake-site", catalog1.Spec.SiteId) assert.Equal(t, "topology", catalog1.Spec.Type) catalog2, err := convertVisualizationPacketToCatalog("fake-site", model.Packet{ Solution: "solution-1", @@ -162,7 +159,6 @@ func TestMergeCatalogsSameKey(t *testing.T) { DataType: "bytes", }) assert.Nil(t, err) - assert.Equal(t, "fake-site", catalog2.Spec.SiteId) assert.Equal(t, "topology", catalog2.Spec.Type) mergedCatalog, err := mergeCatalogs(catalog1, catalog2) @@ -190,7 +186,6 @@ func TestMergeCatalogsDifferentKey(t *testing.T) { DataType: "bytes", }) assert.Nil(t, err) - assert.Equal(t, "fake-site", catalog1.Spec.SiteId) assert.Equal(t, "topology", catalog1.Spec.Type) catalog2, err := convertVisualizationPacketToCatalog("fake-site", model.Packet{ Solution: "solution-1", @@ -202,7 +197,6 @@ func TestMergeCatalogsDifferentKey(t *testing.T) { DataType: "bytes", }) assert.Nil(t, err) - assert.Equal(t, "fake-site", catalog2.Spec.SiteId) assert.Equal(t, "topology", catalog2.Spec.Type) mergedCatalog, err := mergeCatalogs(catalog1, catalog2) diff --git a/docs/samples/approval/logicapp/instance-catalog.yaml b/docs/samples/approval/logicapp/instance-catalog.yaml index d0f8bf856..d5dd70349 100644 --- a/docs/samples/approval/logicapp/instance-catalog.yaml +++ b/docs/samples/approval/logicapp/instance-catalog.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: gated-prometheus-instance -spec: - siteId: hq +spec: type: instance properties: spec: diff --git a/docs/samples/approval/script/instance-catalog.yaml b/docs/samples/approval/script/instance-catalog.yaml index d0f8bf856..d5dd70349 100644 --- a/docs/samples/approval/script/instance-catalog.yaml +++ b/docs/samples/approval/script/instance-catalog.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: gated-prometheus-instance -spec: - siteId: hq +spec: type: instance properties: spec: diff --git a/docs/samples/multisite/catalog-catalog.yaml b/docs/samples/multisite/catalog-catalog.yaml index 472f02307..e6bf85774 100644 --- a/docs/samples/multisite/catalog-catalog.yaml +++ b/docs/samples/multisite/catalog-catalog.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site-catalog -spec: - siteId: hq +spec: type: catalog properties: metadata: diff --git a/docs/samples/multisite/instance-catalog.yaml b/docs/samples/multisite/instance-catalog.yaml index 1e21206a2..6d36603e6 100644 --- a/docs/samples/multisite/instance-catalog.yaml +++ b/docs/samples/multisite/instance-catalog.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site-instance -spec: - siteId: hq +spec: type: instance properties: spec: diff --git a/docs/samples/multisite/solution-catalog.yaml b/docs/samples/multisite/solution-catalog.yaml index 940638c6f..d38d3443b 100644 --- a/docs/samples/multisite/solution-catalog.yaml +++ b/docs/samples/multisite/solution-catalog.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site-app -spec: - siteId: hq +spec: type: solution properties: spec: diff --git a/docs/samples/multisite/target-catalog.yaml b/docs/samples/multisite/target-catalog.yaml index 5a453d0f9..5a2032cd1 100644 --- a/docs/samples/multisite/target-catalog.yaml +++ b/docs/samples/multisite/target-catalog.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site-k8s-target -spec: - siteId: hq +spec: type: target properties: spec: diff --git a/docs/samples/opera/app/types.d.ts b/docs/samples/opera/app/types.d.ts index bb3544d54..a0e65c933 100644 --- a/docs/samples/opera/app/types.d.ts +++ b/docs/samples/opera/app/types.d.ts @@ -46,7 +46,7 @@ export interface CampaignSpec { } export interface ObjectRef { - siteId: string; + siteId: string; name: string; group: string; version: string; @@ -58,7 +58,6 @@ export interface ObjectRef { } export interface CatalogSpec { - siteId: string; name: string; type: string; properties: Record; diff --git a/docs/samples/opera/components/editors/CatalogEditor.tsx b/docs/samples/opera/components/editors/CatalogEditor.tsx index 637a11999..f70c5b4b6 100644 --- a/docs/samples/opera/components/editors/CatalogEditor.tsx +++ b/docs/samples/opera/components/editors/CatalogEditor.tsx @@ -54,7 +54,6 @@ function CatalogEditor(props: CatalogEditorProps) { const formData = new FormData(event.currentTarget); const data = Object.fromEntries(formData.entries()); const catalog: CatalogSpec = { - siteId: process.env.SYMPHONY_SITE || '', name: typeof data.name === 'string' ? data.name : '', parentName: "", type: "config", diff --git a/docs/samples/universe-data/chemical-factory-2/catalogs/assets.yaml b/docs/samples/universe-data/chemical-factory-2/catalogs/assets.yaml index 6fe3e1761..5b0ecf5aa 100644 --- a/docs/samples/universe-data/chemical-factory-2/catalogs/assets.yaml +++ b/docs/samples/universe-data/chemical-factory-2/catalogs/assets.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: hq -spec: - siteId: hq +spec: type: asset properties: name: HQ @@ -21,8 +20,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: infrastructure -spec: - siteId: hq +spec: type: asset properties: name: "Infrastructure" @@ -32,8 +30,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: l3 -spec: - siteId: hq +spec: type: asset properties: name: "Level 3" @@ -43,8 +40,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: l4 -spec: - siteId: hq +spec: type: asset properties: name: "Level 4" @@ -54,8 +50,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: use-cases -spec: - siteId: hq +spec: type: asset properties: name: "Use Cases" @@ -65,8 +60,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: csad -spec: - siteId: hq +spec: type: asset properties: name: "CSAD" @@ -76,8 +70,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site -spec: - siteId: hq +spec: type: asset properties: name: "Site" @@ -87,8 +80,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: line-a -spec: - siteId: hq +spec: type: asset properties: name: "Line A" @@ -98,8 +90,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: line-b -spec: - siteId: hq +spec: type: asset properties: name: "Line B" diff --git a/docs/samples/universe-data/chemical-factory-2/catalogs/configurations.yaml b/docs/samples/universe-data/chemical-factory-2/catalogs/configurations.yaml index 7117cff92..69c4313f5 100644 --- a/docs/samples/universe-data/chemical-factory-2/catalogs/configurations.yaml +++ b/docs/samples/universe-data/chemical-factory-2/catalogs/configurations.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: global-config -spec: - siteId: hq +spec: type: config name: global-config metadata: @@ -17,8 +16,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: l3-config -spec: - siteId: hq +spec: type: config metadata: asset: l3 @@ -31,8 +29,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: l4-config -spec: - siteId: hq +spec: type: config metadata: asset: l4 @@ -45,8 +42,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: csad-config -spec: - siteId: hq +spec: type: config parentName: global-config metadata: @@ -59,8 +55,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site-config -spec: - siteId: hq +spec: type: config metadata: asset: site @@ -74,8 +69,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: line-a-config -spec: - siteId: hq +spec: type: config metadata: asset: line-a @@ -88,8 +82,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: line-b-config -spec: - siteId: hq +spec: type: config metadata: asset: line-b diff --git a/docs/samples/universe-data/chemical-factory/configurations-with-schema.yaml b/docs/samples/universe-data/chemical-factory/configurations-with-schema.yaml index 1a855943a..7ff663618 100644 --- a/docs/samples/universe-data/chemical-factory/configurations-with-schema.yaml +++ b/docs/samples/universe-data/chemical-factory/configurations-with-schema.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: sample-config -spec: - siteId: hq +spec: type: config metadata: schema: sample-schema diff --git a/docs/samples/universe-data/chemical-factory/configurations.yaml b/docs/samples/universe-data/chemical-factory/configurations.yaml index a74e897b2..f1944e07c 100644 --- a/docs/samples/universe-data/chemical-factory/configurations.yaml +++ b/docs/samples/universe-data/chemical-factory/configurations.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: influx-db-config -spec: - siteId: hq +spec: type: config name: influx-db-config properties: @@ -19,8 +18,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: grafana-config -spec: - siteId: hq +spec: type: config properties: host: localhost @@ -33,8 +31,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: e4k-config -spec: - siteId: hq +spec: type: config properties: host: localhost @@ -47,8 +44,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: bluefin-config -spec: - siteId: hq +spec: type: config properties: host: localhost @@ -61,8 +57,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: ai-config -spec: - siteId: hq +spec: type: config metadata: asset: hq @@ -75,8 +70,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: ai-config-site -spec: - siteId: hq +spec: type: config parentName: ai-config metadata: @@ -89,8 +83,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: ai-config-line -spec: - siteId: hq +spec: type: config metadata: asset: line-1 @@ -102,8 +95,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: combined -spec: - siteId: hq +spec: type: config properties: foo: bar @@ -119,8 +111,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: combined-1 -spec: - siteId: hq +spec: type: config properties: foo: .foo @@ -130,8 +121,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: combined-2 -spec: - siteId: hq +spec: type: config properties: foo: bar2 @@ -141,8 +131,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: external -spec: - siteId: hq +spec: type: config properties: foo: far diff --git a/docs/samples/universe-data/chemical-factory/manifests.yaml b/docs/samples/universe-data/chemical-factory/manifests.yaml index 49f2e5337..9565dbf6c 100644 --- a/docs/samples/universe-data/chemical-factory/manifests.yaml +++ b/docs/samples/universe-data/chemical-factory/manifests.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site-app-config -spec: - siteId: hq +spec: type: config name: site-app-config properties: @@ -14,8 +13,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: line-app-config -spec: - siteId: hq +spec: type: config properties: cat: leory @@ -25,8 +23,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site-app -spec: - siteId: hq +spec: type: solution properties: spec: @@ -50,8 +47,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: line-app -spec: - siteId: hq +spec: type: solution properties: spec: @@ -137,8 +133,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: smart-fridge -spec: - siteId: hq +spec: type: solution properties: spec: @@ -164,8 +159,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site-instance -spec: - siteId: hq +spec: type: instance properties: spec: diff --git a/docs/samples/universe-data/chemical-factory/schemas.yaml b/docs/samples/universe-data/chemical-factory/schemas.yaml index 611a8d200..b86fa70ab 100644 --- a/docs/samples/universe-data/chemical-factory/schemas.yaml +++ b/docs/samples/universe-data/chemical-factory/schemas.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: sample-schema -spec: - siteId: hq +spec: type: schema properties: spec: diff --git a/docs/samples/universe-data/chemical-factory/sites.yaml b/docs/samples/universe-data/chemical-factory/sites.yaml index 6c46075e0..9ef9dea57 100644 --- a/docs/samples/universe-data/chemical-factory/sites.yaml +++ b/docs/samples/universe-data/chemical-factory/sites.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: hq -spec: - siteId: hq +spec: type: asset properties: name: HQ @@ -21,8 +20,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: tokyo -spec: - siteId: tokyo +spec: type: asset properties: name: "東京" @@ -40,8 +38,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: new-york -spec: - siteId: new-york +spec: type: asset properties: name: "New York" @@ -59,8 +56,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: munchen -spec: - siteId: munchen +spec: type: asset properties: name: "München" @@ -78,8 +74,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: hq-adr -spec: - siteId: hq +spec: type: asset properties: name: "HQ Azure Device Registry" @@ -99,8 +94,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: hq-arc-1 -spec: - siteId: hq +spec: type: asset properties: name: "HQ Azure Arc Cluster 1" @@ -120,8 +114,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: hq-arc-2 -spec: - siteId: hq +spec: type: asset properties: name: "HQ Azure Arc Cluster 2" @@ -141,8 +134,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: hq-doe-site -spec: - siteId: hq +spec: type: asset properties: name: "HQ DOE Site" @@ -162,8 +154,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: hq-iot-hub -spec: - siteId: hq +spec: type: asset properties: name: "HQ IoT Hub Tenant" @@ -183,8 +174,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: area-1 -spec: - siteId: hq +spec: type: asset properties: name: "Area 1" @@ -194,8 +184,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: area-2 -spec: - siteId: hq +spec: type: asset properties: name: "Area 2" @@ -205,8 +194,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: line-1 -spec: - siteId: hq +spec: type: asset properties: name: "Production Line 1" @@ -216,8 +204,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: line-2 -spec: - siteId: hq +spec: type: asset properties: name: "Production Line 2" diff --git a/docs/symphony-book/api/symphony-api-openapi.yaml b/docs/symphony-book/api/symphony-api-openapi.yaml index 6e7ad0d5d..7a1821827 100644 --- a/docs/symphony-book/api/symphony-api-openapi.yaml +++ b/docs/symphony-book/api/symphony-api-openapi.yaml @@ -411,7 +411,6 @@ paths: schema: type: object example: - siteId: hq name: '{{CATALOG_NAME}}' type: asset properties: @@ -502,7 +501,6 @@ paths: schema: type: object example: - siteId: hq name: '{{CATALOG_NAME}}' type: asset properties: @@ -525,7 +523,6 @@ paths: schema: type: object example: - siteId: hq name: '{{CATALOG_NAME}}-2' type: asset properties: @@ -1562,7 +1559,6 @@ paths: schema: type: object example: - siteId: hq name: '{{CATALOG_NAME}}' type: asset properties: diff --git a/docs/symphony-book/concepts/unified-object-model/catalog.md b/docs/symphony-book/concepts/unified-object-model/catalog.md index 146155ad5..03f038677 100644 --- a/docs/symphony-book/concepts/unified-object-model/catalog.md +++ b/docs/symphony-book/concepts/unified-object-model/catalog.md @@ -10,7 +10,6 @@ kind: Catalog metadata: name: hq spec: - siteId: hq type: asset properties: name: HQ @@ -35,7 +34,6 @@ kind: Catalog metadata: name: edges spec: - siteId: hq type: edge properties: node1: node2 @@ -52,7 +50,6 @@ kind: Catalog metadata: name: app-config spec: - siteId: hq type: config parentName: global-config metadata: diff --git a/docs/symphony-book/configuration-management/define-configurations.md b/docs/symphony-book/configuration-management/define-configurations.md index b73710b17..b464bd253 100644 --- a/docs/symphony-book/configuration-management/define-configurations.md +++ b/docs/symphony-book/configuration-management/define-configurations.md @@ -7,8 +7,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: robot-config -spec: - siteId: hq +spec: type: config properties: name: my-robot diff --git a/k8s/apis/model/v1/common_types.go b/k8s/apis/model/v1/common_types.go index f9d6cbe87..f9b4f0d36 100644 --- a/k8s/apis/model/v1/common_types.go +++ b/k8s/apis/model/v1/common_types.go @@ -151,7 +151,6 @@ type CampaignSpec struct { // +kubebuilder:object:generate=true type CatalogSpec struct { - SiteId string `json:"siteId"` Type string `json:"type"` Metadata map[string]string `json:"metadata,omitempty"` // +kubebuilder:pruning:PreserveUnknownFields diff --git a/k8s/config/oss/crd/bases/federation.symphony_catalogs.yaml b/k8s/config/oss/crd/bases/federation.symphony_catalogs.yaml index defdd5d53..2cef052cf 100644 --- a/k8s/config/oss/crd/bases/federation.symphony_catalogs.yaml +++ b/k8s/config/oss/crd/bases/federation.symphony_catalogs.yaml @@ -75,13 +75,10 @@ spec: properties: type: object x-kubernetes-preserve-unknown-fields: true - siteId: - type: string type: type: string required: - properties - - siteId - type type: object status: diff --git a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index ecd8a43a0..34e8f4c6f 100644 --- a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml @@ -265,13 +265,10 @@ spec: properties: type: object x-kubernetes-preserve-unknown-fields: true - siteId: - type: string type: type: string required: - properties - - siteId - type type: object status: diff --git a/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml b/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml index b0f42a19f..aa157d3c8 100644 --- a/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml +++ b/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: site-instance -spec: - siteId: hq +spec: type: instance properties: spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/asset.yaml b/test/integration/scenarios/05.catalog/catalogs/asset.yaml index 0c35d65bb..5d2135fc9 100644 --- a/test/integration/scenarios/05.catalog/catalogs/asset.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/asset.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: asset -spec: - siteId: hq +spec: type: asset properties: name: "東京" diff --git a/test/integration/scenarios/05.catalog/catalogs/config.yaml b/test/integration/scenarios/05.catalog/catalogs/config.yaml index 88b2899b8..6a5231ddf 100644 --- a/test/integration/scenarios/05.catalog/catalogs/config.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/config.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: config -spec: - siteId: hq +spec: type: config metadata: schema: schema diff --git a/test/integration/scenarios/05.catalog/catalogs/instance.yaml b/test/integration/scenarios/05.catalog/catalogs/instance.yaml index 69f942adb..d77bd9ea0 100644 --- a/test/integration/scenarios/05.catalog/catalogs/instance.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/instance.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: instance -spec: - siteId: hq +spec: type: instance properties: spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/schema.yaml b/test/integration/scenarios/05.catalog/catalogs/schema.yaml index dbcb85085..c3f2faedb 100644 --- a/test/integration/scenarios/05.catalog/catalogs/schema.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/schema.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: schema -spec: - siteId: hq +spec: type: schema properties: spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/solution.yaml b/test/integration/scenarios/05.catalog/catalogs/solution.yaml index a19bb7350..bf04bb044 100644 --- a/test/integration/scenarios/05.catalog/catalogs/solution.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/solution.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: solution -spec: - siteId: hq +spec: type: solution properties: spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/target.yaml b/test/integration/scenarios/05.catalog/catalogs/target.yaml index a16994ef7..c7e42f8b6 100644 --- a/test/integration/scenarios/05.catalog/catalogs/target.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/target.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: target -spec: - siteId: hq +spec: type: target properties: spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/wrongconfig.yaml b/test/integration/scenarios/05.catalog/catalogs/wrongconfig.yaml index 3e3024d76..4a9ea9185 100644 --- a/test/integration/scenarios/05.catalog/catalogs/wrongconfig.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/wrongconfig.yaml @@ -2,8 +2,7 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: name: wrongconfig -spec: - siteId: hq +spec: type: config metadata: schema: schema From 22130ea3ae0a76c51df79bef0335156bee72dc02 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Wed, 8 May 2024 12:09:10 +0200 Subject: [PATCH 16/26] Correctly setup commit user and email in release workflow (#249) Signed-off-by: Thomas Neidhart --- .github/workflows/release.yaml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 58d2434b5..211f8525e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,11 +25,6 @@ jobs: with: token: ${{ secrets.BOT_GITHUB_TOKEN }} - - name: Git config - run: | - git config user.name ${{ env.BOT_USER_NAME }} - git config user.email ${{ env.BOT_EMAIL_ID }} - - name: Install dependencies run: | sudo apt-get update && sudo apt-get install -y make gcc @@ -155,8 +150,8 @@ jobs: - name: Commit changes run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" + git config --local user.name ${{ env.BOT_USER_NAME }} + git config --local user.email ${{ env.BOT_EMAIL_ID }} git add .github/version/versions.txt git add packages/helm/symphony/Chart.yaml git add cli/cmd/up.go From 9b8d91f4ca98a17b48cd760521aa7b262233f0f8 Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Thu, 18 Apr 2024 11:23:22 -0700 Subject: [PATCH 17/26] add experimental notice --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 611b60524..3adc36738 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ _(last edit: 02/02/2024)_ +### ⚠️⚠️⚠️ This branch hosts experimental features. The Symphony community does not guarantee long-term support for these features. Their incorporation into the main branch is not assured, and they may be deprecated or removed at any time without notice. ⚠️⚠️⚠️ + Symphony is a powerful service orchestration engine that enables the organization of multiple intelligent edge services into a seamless, end-to-end experience. Its primary purpose is to address the inherent complexity of edge deployment by providing a set of technology-agnostic workflow APIs, which are designed to deliver a streamlined experience for users across all device profiles. Symphony is uniquely capable of providing consistency across the entire software stack, from drivers to containers to configurations and policies. This comprehensive approach ensures that all aspects of your intelligent edge projects are effectively managed and optimized. Moreover, Symphony provides full support for the entire lifecycle of your edge computing initiatives, spanning from the initial deployment to ongoing updates and maintenance. From 48e9cabf63dd467c5814baa800e939c4a0877a89 Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Fri, 19 Apr 2024 09:19:27 -0700 Subject: [PATCH 18/26] experimental feature table --- README.md | 4 +++- experimental-features.md | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 experimental-features.md diff --git a/README.md b/README.md index 3adc36738..97374fd06 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ _(last edit: 02/02/2024)_ -### ⚠️⚠️⚠️ This branch hosts experimental features. The Symphony community does not guarantee long-term support for these features. Their incorporation into the main branch is not assured, and they may be deprecated or removed at any time without notice. ⚠️⚠️⚠️ +### ⚠️⚠️⚠️ This branch hosts experimental features. The Symphony community does not guarantee long-term support for these features. Their incorporation into the main branch is not assured, and they may be deprecated or removed at any time without notice. For more details, please see [Experimental Feature Table](./experimental-features.md)⚠️⚠️⚠️ + + Symphony is a powerful service orchestration engine that enables the organization of multiple intelligent edge services into a seamless, end-to-end experience. Its primary purpose is to address the inherent complexity of edge deployment by providing a set of technology-agnostic workflow APIs, which are designed to deliver a streamlined experience for users across all device profiles. diff --git a/experimental-features.md b/experimental-features.md new file mode 100644 index 000000000..9d5ebaf04 --- /dev/null +++ b/experimental-features.md @@ -0,0 +1,9 @@ +# Experimental Features + +| Feature | Experimental | Main | +|--------|--------|--------| +| [Proxy Stage Provider](#proxy-stage-provider) | PR #229 | PR #230 | + +* ### Proxy Stage Provider + + Proxy Stage Provider allows a Campaign stage to be executed in an isolated process. See [here](./docs/symphony-book/workflow/provider-proxy.md) for more details. \ No newline at end of file From 7ba255caf8875f596f9032a8b8b51e44d57222ca Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Wed, 24 Apr 2024 10:41:56 +0200 Subject: [PATCH 19/26] experimental --- experimental-features.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/experimental-features.md b/experimental-features.md index 9d5ebaf04..8f8104c06 100644 --- a/experimental-features.md +++ b/experimental-features.md @@ -1,8 +1,11 @@ # Experimental Features -| Feature | Experimental | Main | -|--------|--------|--------| -| [Proxy Stage Provider](#proxy-stage-provider) | PR #229 | PR #230 | +|Area | Feature | Experimental | Main | +|--------|--------|--------|--------| +|Security| [Proxy Stage Provider](#proxy-stage-provider) | PR #229 | PR #230 | +|Security| Target provisioning| | | +|Security| Mutal cert authentication | | + * ### Proxy Stage Provider From 1b69e4b85c101e67eced0e73e4030be3af2b4f57 Mon Sep 17 00:00:00 2001 From: Haishi2016 Date: Fri, 10 May 2024 15:36:59 -0500 Subject: [PATCH 20/26] Proxy processor (#229) * enable proxy stage processor * initial stage isolation docs * udpate k8s types * workflow docs and samples continued * fix update target result (#227) * fix http test --------- Co-authored-by: Jiawei Du <59427055+msftcoderdjw@users.noreply.github.com> --- .../v1alpha1/managers/stage/stage-manager.go | 46 ++++- api/pkg/apis/v1alpha1/model/campaign.go | 14 +- .../v1alpha1/providers/providerfactory.go | 15 ++ .../v1alpha1/providers/stage/proxy/proxy.go | 160 ++++++++++++++++++ .../apis/v1alpha1/providers/stage/stage.go | 6 + api/pkg/apis/v1alpha1/utils/symphony-api.go | 19 +++ .../apis/v1alpha1/vendors/processor-vendor.go | 98 +++++++++++ .../apis/v1alpha1/vendors/vendorfactory.go | 2 + api/symphony-stage-runner.json | 149 ++++++++++++++++ coa/pkg/apis/v1alpha2/bindings/http/http.go | 25 ++- .../apis/v1alpha2/bindings/http/http_test.go | 21 ++- coa/pkg/apis/v1alpha2/events.go | 11 ++ .../samples/campaigns/counter/activation.yaml | 11 ++ docs/samples/campaigns/counter/campaign.yaml | 12 ++ .../campaigns/hello-world/activation.yaml | 9 + .../campaigns/hello-world/campaign.yaml | 11 ++ .../campaigns/stage-runner/activation.yaml | 9 + .../campaigns/stage-runner/campaign.yaml | 17 ++ docs/symphony-book/images/stage-isolation.png | Bin 0 -> 27519 bytes docs/symphony-book/workflow/_overview.md | 34 ++++ .../workflow/counter-provider.md | 5 + .../workflow/define-campaigns.md | 57 +++++++ docs/symphony-book/workflow/delay-provider.md | 13 ++ docs/symphony-book/workflow/error-handling.md | 6 + docs/symphony-book/workflow/mock-provider.md | 25 +++ docs/symphony-book/workflow/provider-proxy.md | 38 +++++ k8s/apis/model/v1/common_types.go | 14 ++ k8s/apis/model/v1/zz_generated.deepcopy.go | 102 ++--------- .../bases/workflow.symphony_campaigns.yaml | 14 ++ .../templates/symphony-core/symphonyk8s.yaml | 14 ++ 30 files changed, 857 insertions(+), 100 deletions(-) create mode 100644 api/pkg/apis/v1alpha1/providers/stage/proxy/proxy.go create mode 100644 api/pkg/apis/v1alpha1/vendors/processor-vendor.go create mode 100644 api/symphony-stage-runner.json create mode 100644 docs/samples/campaigns/counter/activation.yaml create mode 100644 docs/samples/campaigns/counter/campaign.yaml create mode 100644 docs/samples/campaigns/hello-world/activation.yaml create mode 100644 docs/samples/campaigns/hello-world/campaign.yaml create mode 100644 docs/samples/campaigns/stage-runner/activation.yaml create mode 100644 docs/samples/campaigns/stage-runner/campaign.yaml create mode 100644 docs/symphony-book/images/stage-isolation.png create mode 100644 docs/symphony-book/workflow/_overview.md create mode 100644 docs/symphony-book/workflow/counter-provider.md create mode 100644 docs/symphony-book/workflow/define-campaigns.md create mode 100644 docs/symphony-book/workflow/delay-provider.md create mode 100644 docs/symphony-book/workflow/error-handling.md create mode 100644 docs/symphony-book/workflow/mock-provider.md create mode 100644 docs/symphony-book/workflow/provider-proxy.md diff --git a/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go b/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go index dca0fc0ae..3f7970a95 100644 --- a/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go +++ b/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go @@ -50,6 +50,13 @@ func (t *TaskResult) GetError() error { switch sv := v.(type) { case v1alpha2.State: break + case float64: + state := v1alpha2.State(int(sv)) + stateValue := reflect.ValueOf(state) + if stateValue.Type() != reflect.TypeOf(v1alpha2.State(0)) { + return fmt.Errorf("invalid state %v", sv) + } + t.Outputs["__status"] = state case int: state := v1alpha2.State(sv) stateValue := reflect.ValueOf(state) @@ -232,6 +239,7 @@ func (s *StageManager) ResumeStage(status model.ActivationStatus, cam model.Camp TriggeringStage: stage, Schedule: cam.Stages[nextStage].Schedule, Namespace: namespace, + Proxy: cam.Stages[nextStage].Proxy, } log.Debugf(" M (Stage): Activating next stage: %s\n", activationData.Stage) return activationData, nil @@ -323,7 +331,21 @@ func (s *StageManager) HandleDirectTriggerEvent(ctx context.Context, triggerData } var outputs map[string]interface{} - outputs, _, err = provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, triggerData.Inputs) + if triggerData.Proxy != nil { + proxyProvider, err := factory.CreateProvider(triggerData.Proxy.Provider, nil) + if err != nil { + status.Status = v1alpha2.InternalError + status.ErrorMessage = err.Error() + status.IsActive = false + return status + } + if _, ok := proxyProvider.(contexts.IWithManagerContext); ok { + proxyProvider.(contexts.IWithManagerContext).SetContext(s.Manager.Context) + } + outputs, _, err = proxyProvider.(stage.IProxyStageProvider).Process(ctx, *s.Manager.Context, triggerData) + } else { + outputs, _, err = provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, triggerData.Inputs) + } result := TaskResult{ Outputs: outputs, @@ -349,6 +371,7 @@ func (s *StageManager) HandleDirectTriggerEvent(ctx context.Context, triggerData status.Outputs["__status"] = v1alpha2.OK status.Status = v1alpha2.Done status.IsActive = false + return status } func carryOutPutsToErrorStatus(outputs map[string]interface{}, err error, site string) map[string]interface{} { @@ -564,8 +587,23 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca } else { var outputs map[string]interface{} var pause bool - outputs, pause, err = provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, inputCopy) - + if triggerData.Proxy != nil { + proxyProvider, err := factory.CreateProvider(triggerData.Proxy.Provider, nil) + if err != nil { + results <- TaskResult{ + Outputs: nil, + Error: err, + Site: site, + } + return + } + if _, ok := proxyProvider.(contexts.IWithManagerContext); ok { + proxyProvider.(contexts.IWithManagerContext).SetContext(s.Manager.Context) + } + outputs, pause, err = proxyProvider.(stage.IProxyStageProvider).Process(ctx, *s.Manager.Context, triggerData) + } else { + outputs, pause, err = provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, inputCopy) + } if pause { pauseRequested = true } @@ -689,6 +727,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca TriggeringStage: triggerData.Stage, Schedule: nextStage.Schedule, Namespace: triggerData.Namespace, + Proxy: nextStage.Proxy, } } else { status.Status = v1alpha2.InternalError @@ -808,6 +847,7 @@ func (s *StageManager) HandleActivationEvent(ctx context.Context, actData v1alph TriggeringStage: stage, Schedule: stageSpec.Schedule, Namespace: actData.Namespace, + Proxy: stageSpec.Proxy, }, nil } return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("stage %s is not found", stage), v1alpha2.BadRequest) diff --git a/api/pkg/apis/v1alpha1/model/campaign.go b/api/pkg/apis/v1alpha1/model/campaign.go index 474eec48f..1b01bf4b1 100644 --- a/api/pkg/apis/v1alpha1/model/campaign.go +++ b/api/pkg/apis/v1alpha1/model/campaign.go @@ -23,6 +23,7 @@ type ActivationState struct { Spec *ActivationSpec `json:"spec,omitempty"` Status *ActivationStatus `json:"status,omitempty"` } + type StageSpec struct { Name string `json:"name,omitempty"` Contexts string `json:"contexts,omitempty"` @@ -32,6 +33,7 @@ type StageSpec struct { Inputs map[string]interface{} `json:"inputs,omitempty"` HandleErrors bool `json:"handleErrors,omitempty"` Schedule *v1alpha2.ScheduleSpec `json:"schedule,omitempty"` + Proxy *v1alpha2.ProxySpec `json:"proxy,omitempty"` } func (s StageSpec) DeepEquals(other IDeepEquals) (bool, error) { @@ -63,7 +65,17 @@ func (s StageSpec) DeepEquals(other IDeepEquals) (bool, error) { if !reflect.DeepEqual(s.Schedule, otherS.Schedule) { return false, nil } - + if s.Proxy == nil && otherS.Proxy != nil { + return false, nil + } + if s.Proxy != nil && otherS.Proxy == nil { + return false, nil + } + if s.Proxy != nil && otherS.Proxy != nil { + if !reflect.DeepEqual(s.Proxy.Provider, otherS.Proxy.Provider) { + return false, nil + } + } return true, nil } diff --git a/api/pkg/apis/v1alpha1/providers/providerfactory.go b/api/pkg/apis/v1alpha1/providers/providerfactory.go index e59cb457a..984d120ef 100644 --- a/api/pkg/apis/v1alpha1/providers/providerfactory.go +++ b/api/pkg/apis/v1alpha1/providers/providerfactory.go @@ -20,6 +20,7 @@ import ( materialize "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/materialize" mockstage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/mock" patchstage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/patch" + proxystage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/proxy" remotestage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/remote" scriptstage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/script" waitstage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/wait" @@ -163,6 +164,12 @@ func (s SymphonyProviderFactory) CreateProvider(providerType string, config cp.I if err == nil { return mProvider, nil } + case "providers.stage.proxy": + mProvider := &proxystage.ProxyStageProvider{} + err = mProvider.Init(config) + if err == nil { + return mProvider, nil + } case "providers.target.azure.iotedge": mProvider := &iotedge.IoTEdgeTargetProvider{} err = mProvider.Init(config) @@ -639,6 +646,14 @@ func CreateProviderForTargetRole(context *contexts.ManagerContext, role string, } provider.Context = context return provider, nil + case "providers.stage.proxy": + provider := &proxystage.ProxyStageProvider{} + err := provider.InitWithMap(binding.Config) + if err != nil { + return nil, err + } + provider.Context = context + return provider, nil case "providers.stage.counter": provider := &counterstage.CounterStageProvider{} err := provider.InitWithMap(binding.Config) diff --git a/api/pkg/apis/v1alpha1/providers/stage/proxy/proxy.go b/api/pkg/apis/v1alpha1/providers/stage/proxy/proxy.go new file mode 100644 index 000000000..6dc1be823 --- /dev/null +++ b/api/pkg/apis/v1alpha1/providers/stage/proxy/proxy.go @@ -0,0 +1,160 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package proxy + +import ( + "context" + "encoding/json" + "sync" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" +) + +var msLock sync.Mutex +var sLog = logger.NewLogger("coa.runtime") + +type ProxyStageProviderConfig struct { + BaseUrl string `json:"baseUrl"` + User string `json:"user"` + Password string `json:"password"` +} + +type ProxyStageProvider struct { + Config ProxyStageProviderConfig + Context *contexts.ManagerContext +} + +func (s *ProxyStageProvider) Init(config providers.IProviderConfig) error { + msLock.Lock() + defer msLock.Unlock() + mockConfig, err := toProxyStageProviderConfig(config) + if err != nil { + return err + } + s.Config = mockConfig + return nil +} +func (s *ProxyStageProvider) SetContext(ctx *contexts.ManagerContext) { + s.Context = ctx +} +func toProxyStageProviderConfig(config providers.IProviderConfig) (ProxyStageProviderConfig, error) { + ret := ProxyStageProviderConfig{} + data, err := json.Marshal(config) + if err != nil { + return ret, err + } + err = json.Unmarshal(data, &ret) + return ret, err +} +func (i *ProxyStageProvider) InitWithMap(properties map[string]string) error { + config, err := SymphonyStageProviderConfigFromMap(properties) + if err != nil { + return err + } + return i.Init(config) +} +func SymphonyStageProviderConfigFromMap(properties map[string]string) (ProxyStageProviderConfig, error) { + ret := ProxyStageProviderConfig{} + baseUrl, err := utils.GetString(properties, "baseUrl") + if err != nil { + return ret, err + } + ret.BaseUrl = baseUrl + if ret.BaseUrl == "" { + return ret, v1alpha2.NewCOAError(nil, "baseUrl is required", v1alpha2.BadConfig) + } + user, err := utils.GetString(properties, "user") + if err != nil { + return ret, err + } + ret.User = user + if ret.User == "" { + return ret, v1alpha2.NewCOAError(nil, "user is required", v1alpha2.BadConfig) + } + password, err := utils.GetString(properties, "password") + if err != nil { + return ret, err + } + ret.Password = password + return ret, nil +} +func (m *ProxyStageProvider) traceValue(v interface{}, ctx interface{}) (interface{}, error) { + switch val := v.(type) { + case string: + parser := utils.NewParser(val) + context := m.Context.VencorContext.EvaluationContext.Clone() + context.Value = ctx + v, err := parser.Eval(*context) + if err != nil { + return "", err + } + switch vt := v.(type) { + case string: + return vt, nil + default: + return m.traceValue(v, ctx) + } + case []interface{}: + ret := []interface{}{} + for _, v := range val { + tv, err := m.traceValue(v, ctx) + if err != nil { + return "", err + } + ret = append(ret, tv) + } + return ret, nil + case map[string]interface{}: + ret := map[string]interface{}{} + for k, v := range val { + tv, err := m.traceValue(v, ctx) + if err != nil { + return "", err + } + ret[k] = tv + } + return ret, nil + default: + return val, nil + } +} + +func (i *ProxyStageProvider) Process(ctx context.Context, mgrContext contexts.ManagerContext, activationdata v1alpha2.ActivationData) (map[string]interface{}, bool, error) { + ctx, span := observability.StartSpan("[Stage] Proxy Provider", ctx, &map[string]string{ + "method": "Process", + }) + var err error = nil + var ret model.ActivationStatus + defer observ_utils.CloseSpanWithError(span, &err) + + sLog.Info(" P (Proxy Stage): start process request") + + ret, err = utils.CallRemoteProcessor(ctx, + activationdata.Proxy.Config.BaseUrl, + activationdata.Proxy.Config.User, + activationdata.Proxy.Config.Password, + activationdata) + if err != nil { + sLog.Errorf(" P (Proxy Stage): error calling remote stage processor %s", err.Error()) + return nil, false, err + } + if ret.ErrorMessage != "" { + sLog.Errorf(" P (Proxy Stage): remote stage processor returned an error %s", ret.ErrorMessage) + return nil, false, v1alpha2.NewCOAError(nil, ret.ErrorMessage, v1alpha2.InternalError) + } + outputs := ret.Outputs + + sLog.Info(" P (Proxy Stage): end process request") + return outputs, false, nil +} diff --git a/api/pkg/apis/v1alpha1/providers/stage/stage.go b/api/pkg/apis/v1alpha1/providers/stage/stage.go index 95fe82cbd..53ef862dd 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/stage.go +++ b/api/pkg/apis/v1alpha1/providers/stage/stage.go @@ -9,6 +9,7 @@ package stage import ( "context" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" ) @@ -17,6 +18,11 @@ type IStageProvider interface { Process(ctx context.Context, mgrContext contexts.ManagerContext, inputs map[string]interface{}) (map[string]interface{}, bool, error) } +type IProxyStageProvider interface { + // Return values: map[string]interface{} - outputs, bool - should the activation be paused (wait for a remote event), error + Process(ctx context.Context, mgrContext contexts.ManagerContext, activationdata v1alpha2.ActivationData) (map[string]interface{}, bool, error) +} + func ReadInputString(inputs map[string]interface{}, key string) string { if inputs == nil { return "" diff --git a/api/pkg/apis/v1alpha1/utils/symphony-api.go b/api/pkg/apis/v1alpha1/utils/symphony-api.go index e10d19911..51f143851 100644 --- a/api/pkg/apis/v1alpha1/utils/symphony-api.go +++ b/api/pkg/apis/v1alpha1/utils/symphony-api.go @@ -250,6 +250,25 @@ func PublishActivationEvent(context context.Context, baseUrl string, user string return nil } +func CallRemoteProcessor(context context.Context, baseUrl string, user string, password string, event v1alpha2.ActivationData) (model.ActivationStatus, error) { + ret := model.ActivationStatus{} + token, err := auth(context, baseUrl, user, password) + + if err != nil { + return ret, err + } + event.Proxy = nil + jData, _ := json.Marshal(event) + response, err := callRestAPI(context, baseUrl, "processor", "POST", jData, token) + if err != nil { + return ret, err + } + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + return ret, nil +} func GetABatchForSite(context context.Context, baseUrl string, site string, user string, password string) (model.SyncPackage, error) { ret := model.SyncPackage{} token, err := auth(context, baseUrl, user, password) diff --git a/api/pkg/apis/v1alpha1/vendors/processor-vendor.go b/api/pkg/apis/v1alpha1/vendors/processor-vendor.go new file mode 100644 index 000000000..bfbf997f5 --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/processor-vendor.go @@ -0,0 +1,98 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "encoding/json" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/stage" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/valyala/fasthttp" +) + +type ProcessorVendor struct { + vendors.Vendor + StageManager *stage.StageManager +} + +func (o *ProcessorVendor) GetInfo() vendors.VendorInfo { + return vendors.VendorInfo{ + Version: o.Vendor.Version, + Name: "Processor", + Producer: "Microsoft", + } +} + +func (e *ProcessorVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error { + err := e.Vendor.Init(config, factories, providers, pubsubProvider) + if err != nil { + return err + } + for _, m := range e.Managers { + if c, ok := m.(*stage.StageManager); ok { + e.StageManager = c + } + } + if e.StageManager == nil { + return v1alpha2.NewCOAError(nil, "stage manager is not supplied", v1alpha2.MissingConfig) + } + return nil +} + +func (o *ProcessorVendor) GetEndpoints() []v1alpha2.Endpoint { + route := "processor" + if o.Route != "" { + route = o.Route + } + return []v1alpha2.Endpoint{ + { + Methods: []string{fasthttp.MethodPost}, + Route: route, + Version: o.Version, + Handler: o.onProcess, + }, + } +} + +func (c *ProcessorVendor) onProcess(request v1alpha2.COARequest) v1alpha2.COAResponse { + ctx, span := observability.StartSpan("Processor Vendor", request.Context, &map[string]string{ + "method": "onProcess", + }) + defer span.End() + + switch request.Method { + case fasthttp.MethodPost: + triggerData := v1alpha2.ActivationData{} + err := json.Unmarshal(request.Body, &triggerData) + if err != nil { + tLog.Infof("V (Processor) : onProcess failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + status := c.StageManager.HandleDirectTriggerEvent(ctx, triggerData) + jData, _ := json.Marshal(status) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + Body: jData, + }) + } + resp := v1alpha2.COAResponse{ + State: v1alpha2.MethodNotAllowed, + Body: []byte("{\"result\":\"405 - method not allowed\"}"), + ContentType: "application/json", + } + observ_utils.UpdateSpanStatusFromCOAResponse(span, resp) + return resp +} diff --git a/api/pkg/apis/v1alpha1/vendors/vendorfactory.go b/api/pkg/apis/v1alpha1/vendors/vendorfactory.go index 250490797..5f2b58fec 100644 --- a/api/pkg/apis/v1alpha1/vendors/vendorfactory.go +++ b/api/pkg/apis/v1alpha1/vendors/vendorfactory.go @@ -59,6 +59,8 @@ func (c SymphonyVendorFactory) CreateVendor(config vendors.VendorConfig) (vendor return &VisualizationClientVendor{}, nil case "vendors.visualization": return &VisualizationVendor{}, nil + case "vendors.processor": + return &ProcessorVendor{}, nil default: return nil, nil //Can't throw errors as other factories may create it... } diff --git a/api/symphony-stage-runner.json b/api/symphony-stage-runner.json new file mode 100644 index 000000000..5c2669915 --- /dev/null +++ b/api/symphony-stage-runner.json @@ -0,0 +1,149 @@ +{ + "siteInfo": { + "siteId": "symphony-proxy", + "currentSite": { + "baseUrl": "http://localhost:8098/v1alpha2/", + "username": "", + "password": "" + } + }, + "api": { + "pubsub": { + "shared": true, + "provider": { + "type": "providers.pubsub.memory", + "config": {} + } + }, + "vendors": [ + { + "type": "vendors.echo", + "route": "greetings", + "managers": [] + }, + { + "type": "vendors.users", + "loopInterval": 15, + "route": "users", + "properties": { + "test-users": "true" + }, + "managers": [ + { + "name": "users-manager", + "type": "managers.symphony.users", + "properties": { + "providers.state": "mem-state" + }, + "providers": { + "mem-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, + { + "type": "vendors.processor", + "route": "processor", + "managers": [ + { + "name": "processor-manager", + "type": "managers.symphony.stage", + "properties": { + "providers.state": "mem-state" + }, + "providers": { + "mem-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + } + ] + }, + "bindings": [ + { + "type": "bindings.http", + "config": { + "port": 9082, + "pipeline": [ + { + "type": "middleware.http.cors", + "properties": { + "Access-Control-Allow-Headers": "authorization,Content-Type", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Origin": "*" + } + }, + { + "type": "middleware.http.jwt", + "properties": { + "ignorePaths": ["/v1alpha2/users/auth", "/v1alpha2/solution/instances", "/v1alpha2/agent/references", "/v1alpha2/greetings"], + "verifyKey": "SymphonyKey", + "enableRBAC": true, + "roles": [ + { + "role": "administrator", + "claim": "user", + "value": "admin" + }, + { + "role": "reader", + "claim": "user", + "value": "*" + }, + { + "role": "solution-creator", + "claim": "user", + "value": "developer" + }, + { + "role": "target-manager", + "claim": "user", + "value": "device-manager" + }, + { + "role": "operator", + "claim": "user", + "value": "solution-operator" + } + ], + "policy": { + "administrator": { + "items": { + "*": "*" + } + }, + "reader": { + "items": { + "*": "GET" + } + }, + "solution-creator": { + "items": { + "/v1alpha2/solutions": "*" + } + }, + "target-manager": { + "items": { + "/v1alpha2/targets": "*" + } + }, + "solution-operator": { + "items": { + "/v1alpha2/instances": "*" + } + } + } + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/coa/pkg/apis/v1alpha2/bindings/http/http.go b/coa/pkg/apis/v1alpha2/bindings/http/http.go index 044933598..bc7298112 100644 --- a/coa/pkg/apis/v1alpha2/bindings/http/http.go +++ b/coa/pkg/apis/v1alpha2/bindings/http/http.go @@ -160,7 +160,30 @@ func wrapAsHTTPHandler(endpoint v1alpha2.Endpoint, handler v1alpha2.COAHandler) } reqCtx.SetContentType(resp.ContentType) reqCtx.SetBody(resp.Body) - reqCtx.SetStatusCode(int(resp.State)) + reqCtx.SetStatusCode(toHttpState(resp.State)) } } } + +func toHttpState(state v1alpha2.State) int { + switch state { + case v1alpha2.OK: + return fasthttp.StatusOK + case v1alpha2.Accepted: + return fasthttp.StatusAccepted + case v1alpha2.BadRequest: + return fasthttp.StatusBadRequest + case v1alpha2.Unauthorized: + return fasthttp.StatusUnauthorized + case v1alpha2.NotFound: + return fasthttp.StatusNotFound + case v1alpha2.MethodNotAllowed: + return fasthttp.StatusMethodNotAllowed + case v1alpha2.Conflict: + return fasthttp.StatusConflict + case v1alpha2.InternalError: + return fasthttp.StatusInternalServerError + default: + return fasthttp.StatusInternalServerError + } +} diff --git a/coa/pkg/apis/v1alpha2/bindings/http/http_test.go b/coa/pkg/apis/v1alpha2/bindings/http/http_test.go index ade926a0d..0d5ff7b2e 100644 --- a/coa/pkg/apis/v1alpha2/bindings/http/http_test.go +++ b/coa/pkg/apis/v1alpha2/bindings/http/http_test.go @@ -93,7 +93,8 @@ func TestHTTPEcho(t *testing.T) { Version: "v1", Handler: func(c v1alpha2.COARequest) v1alpha2.COAResponse { return v1alpha2.COAResponse{ - Body: []byte("Hi there!!"), + Body: []byte("Hi there!!"), + State: v1alpha2.OK, } }, }, @@ -103,7 +104,8 @@ func TestHTTPEcho(t *testing.T) { Version: "v1", Handler: func(c v1alpha2.COARequest) v1alpha2.COAResponse { return v1alpha2.COAResponse{ - Body: []byte("Hi " + c.Parameters["name"] + "!!"), + Body: []byte("Hi " + c.Parameters["name"] + "!!"), + State: v1alpha2.OK, } }, }, @@ -114,7 +116,8 @@ func TestHTTPEcho(t *testing.T) { Parameters: []string{"name"}, Handler: func(c v1alpha2.COARequest) v1alpha2.COAResponse { return v1alpha2.COAResponse{ - Body: []byte("Hi " + c.Parameters["__name"] + "!!!"), + Body: []byte("Hi " + c.Parameters["__name"] + "!!!"), + State: v1alpha2.OK, } }, }, @@ -129,7 +132,8 @@ func TestHTTPEcho(t *testing.T) { Metadata: map[string]string{ "key": value, }, - Body: []byte("Hi " + value + "!!!!"), + Body: []byte("Hi " + value + "!!!!"), + State: v1alpha2.OK, } }, }, @@ -188,7 +192,8 @@ func TestHTTPEchoWithTLS(t *testing.T) { Version: "v1", Handler: func(c v1alpha2.COARequest) v1alpha2.COAResponse { return v1alpha2.COAResponse{ - Body: []byte("Hi there!!"), + Body: []byte("Hi there!!"), + State: v1alpha2.OK, } }, }, @@ -340,12 +345,14 @@ func TestHTTPEchoWithPipeline(t *testing.T) { switch c.Method { case fasthttp.MethodGet: return v1alpha2.COAResponse{ - Body: []byte("Hi there!!"), + Body: []byte("Hi there!!"), + State: v1alpha2.OK, } case fasthttp.MethodPost: reqBody := string(c.Body) return v1alpha2.COAResponse{ - Body: []byte(fmt.Sprintf("Hi %s!!", reqBody)), + Body: []byte(fmt.Sprintf("Hi %s!!", reqBody)), + State: v1alpha2.OK, } } return v1alpha2.COAResponse{} diff --git a/coa/pkg/apis/v1alpha2/events.go b/coa/pkg/apis/v1alpha2/events.go index 6302e6212..858ffcea3 100644 --- a/coa/pkg/apis/v1alpha2/events.go +++ b/coa/pkg/apis/v1alpha2/events.go @@ -50,6 +50,7 @@ type ActivationData struct { TriggeringStage string `json:"triggeringStage,omitempty"` Schedule *ScheduleSpec `json:"schedule,omitempty"` NeedsReport bool `json:"needsReport,omitempty"` + Proxy *ProxySpec `json:"proxy,omitempty"` } type HeartBeatAction string @@ -71,6 +72,16 @@ type ScheduleSpec struct { Zone string `json:"zone"` } +type ProxyConfigSpec struct { + BaseUrl string `json:"baseUrl,omitempty"` + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` +} +type ProxySpec struct { + Provider string `json:"provider,omitempty"` + Config ProxyConfigSpec `json:"config,omitempty"` +} + func (s ScheduleSpec) ShouldFireNow() (bool, error) { dt, err := s.GetTime() if err != nil { diff --git a/docs/samples/campaigns/counter/activation.yaml b/docs/samples/campaigns/counter/activation.yaml new file mode 100644 index 000000000..70dca5b7c --- /dev/null +++ b/docs/samples/campaigns/counter/activation.yaml @@ -0,0 +1,11 @@ +apiVersion: workflow.symphony/v1 +kind: Activation +metadata: + name: counter-activation +spec: + campaign: "counter-campaign" + name: "counter-activation" + stage: "" + inputs: + val.init: 10 + val: 2 \ No newline at end of file diff --git a/docs/samples/campaigns/counter/campaign.yaml b/docs/samples/campaigns/counter/campaign.yaml new file mode 100644 index 000000000..8806f98f2 --- /dev/null +++ b/docs/samples/campaigns/counter/campaign.yaml @@ -0,0 +1,12 @@ +apiVersion: workflow.symphony/v1 +kind: Campaign +metadata: + name: counter-campaign +spec: + firstStage: "counter" + selfDriving: true + stages: + counter: + name: "counter" + provider: "providers.stage.counter" + stageSelector: "${{$if($lt($output(counter,val), 20), counter, '')}}" \ No newline at end of file diff --git a/docs/samples/campaigns/hello-world/activation.yaml b/docs/samples/campaigns/hello-world/activation.yaml new file mode 100644 index 000000000..a5e1681a1 --- /dev/null +++ b/docs/samples/campaigns/hello-world/activation.yaml @@ -0,0 +1,9 @@ +apiVersion: workflow.symphony/v1 +kind: Activation +metadata: + name: hello-world-activation +spec: + campaign: "hello-world" + name: "hello-world-activation" + inputs: + foo: "bar" \ No newline at end of file diff --git a/docs/samples/campaigns/hello-world/campaign.yaml b/docs/samples/campaigns/hello-world/campaign.yaml new file mode 100644 index 000000000..b66a793a4 --- /dev/null +++ b/docs/samples/campaigns/hello-world/campaign.yaml @@ -0,0 +1,11 @@ +apiVersion: workflow.symphony/v1 +kind: Campaign +metadata: + name: hello-world +spec: + firstStage: "mock" + selfDriving: true + stages: + mock: + name: "mock" + provider: "providers.stage.mock" \ No newline at end of file diff --git a/docs/samples/campaigns/stage-runner/activation.yaml b/docs/samples/campaigns/stage-runner/activation.yaml new file mode 100644 index 000000000..efda029d8 --- /dev/null +++ b/docs/samples/campaigns/stage-runner/activation.yaml @@ -0,0 +1,9 @@ +apiVersion: workflow.symphony/v1 +kind: Activation +metadata: + name: hello-world-runner-activation +spec: + campaign: "hello-world-runner" + name: "hello-world-runner-activation" + inputs: + foo: "bar" \ No newline at end of file diff --git a/docs/samples/campaigns/stage-runner/campaign.yaml b/docs/samples/campaigns/stage-runner/campaign.yaml new file mode 100644 index 000000000..668aca2b0 --- /dev/null +++ b/docs/samples/campaigns/stage-runner/campaign.yaml @@ -0,0 +1,17 @@ +apiVersion: workflow.symphony/v1 +kind: Campaign +metadata: + name: hello-world-runner +spec: + firstStage: "mock" + selfDriving: true + stages: + mock: + name: "mock" + provider: "providers.stage.mock" + proxy: + provider: providers.stage.proxy + config: + baseUrl: http://localhost:9082/v1alpha2/ + user: admin + password: "" \ No newline at end of file diff --git a/docs/symphony-book/images/stage-isolation.png b/docs/symphony-book/images/stage-isolation.png new file mode 100644 index 0000000000000000000000000000000000000000..6fd18a345bdc81daeb1a553f7dd9b434d0139543 GIT binary patch literal 27519 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 literal 0 HcmV?d00001 diff --git a/docs/symphony-book/workflow/_overview.md b/docs/symphony-book/workflow/_overview.md new file mode 100644 index 000000000..f370a53a8 --- /dev/null +++ b/docs/symphony-book/workflow/_overview.md @@ -0,0 +1,34 @@ +# Workflow + +Symphony has a built-in workflow engine that captures complex operational workflows into declarative models. In addition to basic workflow engine features like branches, conditions, loops and stateful stages, Symphony also supports a set of unique workflow features to enable large-scale, automated operations, including: +* Remote execution of stages. +* Scheduled execution. +* Isolated execution environment per workflow stage. +* Fan-out execution of stages (map-reduce). + +## Fundamentals + +* [Defining and running a workflow](./define-campaigns.md) +* Inputs and outputs +* States +* Branches and loops + +## Stage Processors + +* [Counter Stage Provider](./counter-provider.md) +* [Delay Stage Provider](./delay-provider.md) +* [Mock Stage Provider](./mock-provider.md) + +## Advanced + +* Scheduling +* [Error handling and retries](./error-handling.md) +* [Stage isolation with provider proxy](./provider-proxy.md) +* Remote execution +* Fan-out execution + +## Scenarios + +* [Canary Deployment](../scenarios/canary-deployment.md) +* [Approval with Logic Apps](../scenarios/gated-deployment-logic-app.md) +* [Approval with a custom script](../scenarios/gated-deployment-script.md) \ No newline at end of file diff --git a/docs/symphony-book/workflow/counter-provider.md b/docs/symphony-book/workflow/counter-provider.md new file mode 100644 index 000000000..b46f17e02 --- /dev/null +++ b/docs/symphony-book/workflow/counter-provider.md @@ -0,0 +1,5 @@ +# Counter Stage Provider + +The counter stage provider is able to increment one or multiple fields by a given step. For example, if you invoke a counter stage provider with input `foo=1,foo.init=5`, the output `foo` value is `6`, which is `foo.init + foo = 5 + 1 = 6`. + + diff --git a/docs/symphony-book/workflow/define-campaigns.md b/docs/symphony-book/workflow/define-campaigns.md new file mode 100644 index 000000000..6a6b639e4 --- /dev/null +++ b/docs/symphony-book/workflow/define-campaigns.md @@ -0,0 +1,57 @@ +# Defining and running a workflow + +In Symphony, a workflow is described by a `Campaign` object. A campaign contains one or more stages. Each stage is processed by a stage processor. After each stage, a stage selector is evaluated to select the next stage. When no next stages are selected, the campaign finishes. + +The following example shows a simple Symphony campaign with a single stage. The stage is handled by a mock processor that simply generates some outputs in Symphony logs. +```yaml +apiVersion: workflow.symphony/v1 +kind: Campaign +metadata: + name: hello-world +spec: + firstStage: "mock" + selfDriving: true + stages: + mock: + name: "mock" + provider: "providers.stage.mock" +``` +A campaign defines a workflow. To execute a workflow, you create an `Activation` object. A campaign can be activated multiple times. Activations are retained for 24 hours by default. To activate the above workflow, create a new Activation object like the following: +```yaml +apiVersion: workflow.symphony/v1 +kind: Activation +metadata: + name: hello-world-activation +spec: + campaign: "hello-world" + name: "hello-world-activation" + inputs: + foo: "bar" +``` +If you observe Symphony API logs, you can find the outputs from the mock stage: +```txt +==================================================== +MOCK STAGE PROVIDER IS PROCESSING INPUTS: +__activation: hello-world-activation +__stage: mock +__activationGeneration: 1 +__previousStage: mock +__site: hq +foo: bar +__campaign: hello-world +__namespace: +---------------------------------------- +TIME (UTC) : 2024-04-07T02:02:32Z +TIME (Local): 2024-04-07T02:02:32Z +---------------------------------------- +MOCK STAGE PROVIDER IS DONE PROCESSING WITH OUTPUTS: +__activation: hello-world-activation +__stage: mock +__activationGeneration: 1 +__previousStage: mock +__site: hq +foo: bar +__campaign: hello-world +__namespace: +==================================================== +``` \ No newline at end of file diff --git a/docs/symphony-book/workflow/delay-provider.md b/docs/symphony-book/workflow/delay-provider.md new file mode 100644 index 000000000..b3fa05d0f --- /dev/null +++ b/docs/symphony-book/workflow/delay-provider.md @@ -0,0 +1,13 @@ +# Delay Stage Provider + +The delay stage provider pause workflow execution for the given period of time, specified either as an integer in seconds or a [duration expression](https://pkg.go.dev/maze.io/x/duration#ParseDuration) such as `2h45m` (2 hours and 45 minutes). + +The following sample delay stage delays for 30 seconds: + +```yaml +delay: + name: delay + provider: providers.stage.delay + inputs: + delay: 30s +``` diff --git a/docs/symphony-book/workflow/error-handling.md b/docs/symphony-book/workflow/error-handling.md new file mode 100644 index 000000000..31f51d512 --- /dev/null +++ b/docs/symphony-book/workflow/error-handling.md @@ -0,0 +1,6 @@ +# Error Handling and Retries + +A campaign activation stops running if any of the stages fails. However, in some cases you may want to run some error-handling logic when a stage fails. To mark a stage as an error-handler, you can annotate the stage with a `handleErrors` attribute. When a stage fails, its stage selector is evaluated. If the selected stage is an error-handling stage, then the error-handler is executed. Otherwise the activation fails. Once the error-handling stage is executed, the activation is allowed to continue as normal. + +The error-handling stage can be useful in many scenarios, such as sending a notification to a user if a deployment fails or retrying an operation several times. + diff --git a/docs/symphony-book/workflow/mock-provider.md b/docs/symphony-book/workflow/mock-provider.md new file mode 100644 index 000000000..8e93f375a --- /dev/null +++ b/docs/symphony-book/workflow/mock-provider.md @@ -0,0 +1,25 @@ +# Mock Stage Provider + +Mock stage provider is a simple provider for testing. It replays all `inputs` in its `outputs` with one exception: If your `inputs` contains a `foo` field with an integer value, it will increment the field by `1`. For example, if you have input `foo=100`, you'll get output `foo=101`. + +A sample state with a mock provider: + +```yaml +stages: +mock: + name: "mock" + provider: "providers.stage.mock" +``` + +## Use mock stage provider to construct a loop + +In the following stage definition, the ouput field `foo` is fed back to the stage input. And becaue the mock stage provider increments the `foo` value, the output `foo` will be incremented by `1` at each iteration. The stage selector checks the output value and select the `mock` stage again if the value of `foo` is less than `5`. Otherwise, the selector selects an empty stage, which stops the workflow execution. + +```yaml +mock: + name: "mock" + provider: "providers.stage.mock" + inputs: + foo: "${{$output(mock,foo)}}" + stageSelector: "${{$if($lt($output(mock,foo), 5), mock, '')}}" +``` \ No newline at end of file diff --git a/docs/symphony-book/workflow/provider-proxy.md b/docs/symphony-book/workflow/provider-proxy.md new file mode 100644 index 000000000..1b6c08ea4 --- /dev/null +++ b/docs/symphony-book/workflow/provider-proxy.md @@ -0,0 +1,38 @@ +# Stage Isolation With Provider Proxy + +By default, all stage providers are invoked as in-proc calls on the Symphony control plane. This means all stage providers use the same service account context configured for the Symphony API. As the control plane often needs to manage a large number of resources, having a super user with access to all resources is an obvious security concern. A stage provider proxy allows a stage to be processed in an isolated environment such as a separate process, container, virtual machine, or physic device. The stage provider proxy expects a web server (called a **stage runner**) that implements the required Symphony stage provider interface. Although you can use your own stage runner implementations, we recommend using the default Symphony implementation that supports all existing Symphony stage providers to be used over the proxy. + +Such isolation has some distinct benefits: + +* Support different execution environments. As a stage runner can be hosted independently from the control plane, the stage runner can be configured with the exact toolchains for the specific stages. For example, a containerized stage runner can have all necessary tools pre-installed. Another example is that a Windows-based stage runner can use Windows toolchains. +* Because a stage runner runs in a different process, you can assign just enough access rights to the process to perform stage activities. +* The isolation also provides certain protection over vouge provider implementations, such as script-based attacks. +* Resources required by the runner can be mounted locally without needing to be shared with the control plane. + +The following diagram illustrates how stage isolation works with the provider proxy and a stage runner: + +![stage isolation](../images/stage-isolation.png) + +## Decarling stage proxy + +You can attach a proxy setting to any of the stage specs. For example, the follow stage spec specifies that the mock stage should be carried out remotely through a processor proxy: + +```yaml +stages: + mock: + name: mock + provider: providers.stage.mock + proxy: + provider: providers.stage.proxy + config: + baseUrl: http://localhost:9082/v1alpha2/ + user: admin + password: "" +``` + +## Launch a stage runner using Symphony API binary + +You can launch a stage runner by launching the `symphony-api` process with a `symphony-processor-server.json` config: +```bash +./symphony-api -c ./symphony-processor-server.json -l Debug +``` \ No newline at end of file diff --git a/k8s/apis/model/v1/common_types.go b/k8s/apis/model/v1/common_types.go index f9b4f0d36..b5bdbf178 100644 --- a/k8s/apis/model/v1/common_types.go +++ b/k8s/apis/model/v1/common_types.go @@ -115,6 +115,19 @@ type ScheduleSpec struct { Zone string `json:"zone"` } +// +kubebuilder:object:generate=true +type ProxyConfigSpec struct { + BaseUrl string `json:"baseUrl,omitempty"` + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` +} + +// +kubebuilder:object:generate=true +type ProxySpec struct { + Provider string `json:"provider,omitempty"` + Config ProxyConfigSpec `json:"config,omitempty"` +} + // +kubebuilder:object:generate=true type StageSpec struct { Name string `json:"name,omitempty"` @@ -129,6 +142,7 @@ type StageSpec struct { Inputs runtime.RawExtension `json:"inputs,omitempty"` TriggeringStage string `json:"triggeringStage,omitempty"` Schedule *ScheduleSpec `json:"schedule,omitempty"` + Proxy *ProxySpec `json:"proxy,omitempty"` } // +kubebuilder:object:generate=true diff --git a/k8s/apis/model/v1/zz_generated.deepcopy.go b/k8s/apis/model/v1/zz_generated.deepcopy.go index ba92cf76d..6c9657993 100644 --- a/k8s/apis/model/v1/zz_generated.deepcopy.go +++ b/k8s/apis/model/v1/zz_generated.deepcopy.go @@ -125,111 +125,32 @@ func (in *ComponentSpec) DeepCopy() *ComponentSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DeployableStatus) DeepCopyInto(out *DeployableStatus) { +func (in *ProxyConfigSpec) DeepCopyInto(out *ProxyConfigSpec) { *out = *in - if in.Properties != nil { - in, out := &in.Properties, &out.Properties - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - in.ProvisioningStatus.DeepCopyInto(&out.ProvisioningStatus) - in.LastModified.DeepCopyInto(&out.LastModified) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployableStatus. -func (in *DeployableStatus) DeepCopy() *DeployableStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyConfigSpec. +func (in *ProxyConfigSpec) DeepCopy() *ProxyConfigSpec { if in == nil { return nil } - out := new(DeployableStatus) + out := new(ProxyConfigSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *InstanceSpec) DeepCopyInto(out *InstanceSpec) { +func (in *ProxySpec) DeepCopyInto(out *ProxySpec) { *out = *in - if in.Parameters != nil { - in, out := &in.Parameters, &out.Parameters - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Metadata != nil { - in, out := &in.Metadata, &out.Metadata - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - in.Target.DeepCopyInto(&out.Target) - if in.Topologies != nil { - in, out := &in.Topologies, &out.Topologies - *out = make([]model.TopologySpec, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Pipelines != nil { - in, out := &in.Pipelines, &out.Pipelines - *out = make([]model.PipelineSpec, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Arguments != nil { - in, out := &in.Arguments, &out.Arguments - *out = make(map[string]map[string]string, len(*in)) - for key, val := range *in { - var outVal map[string]string - if val == nil { - (*out)[key] = nil - } else { - in, out := &val, &outVal - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - (*out)[key] = outVal - } - } - if in.ReconciliationPolicy != nil { - in, out := &in.ReconciliationPolicy, &out.ReconciliationPolicy - *out = new(ReconciliationPolicySpec) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceSpec. -func (in *InstanceSpec) DeepCopy() *InstanceSpec { - if in == nil { - return nil - } - out := new(InstanceSpec) - in.DeepCopyInto(out) - return out + out.Config = in.Config } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ReconciliationPolicySpec) DeepCopyInto(out *ReconciliationPolicySpec) { - *out = *in - if in.Interval != nil { - in, out := &in.Interval, &out.Interval - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconciliationPolicySpec. -func (in *ReconciliationPolicySpec) DeepCopy() *ReconciliationPolicySpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxySpec. +func (in *ProxySpec) DeepCopy() *ProxySpec { if in == nil { return nil } - out := new(ReconciliationPolicySpec) + out := new(ProxySpec) in.DeepCopyInto(out) return out } @@ -304,6 +225,11 @@ func (in *StageSpec) DeepCopyInto(out *StageSpec) { *out = new(ScheduleSpec) **out = **in } + if in.Proxy != nil { + in, out := &in.Proxy, &out.Proxy + *out = new(ProxySpec) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StageSpec. diff --git a/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml b/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml index 28d8821a9..31c7a52fe 100644 --- a/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml +++ b/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml @@ -53,6 +53,20 @@ spec: type: string provider: type: string + proxy: + properties: + config: + properties: + baseUrl: + type: string + password: + type: string + user: + type: string + type: object + provider: + type: string + type: object schedule: properties: date: diff --git a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index 34e8f4c6f..fa9a3ef6c 100644 --- a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml @@ -154,6 +154,20 @@ spec: type: string provider: type: string + proxy: + properties: + config: + properties: + baseUrl: + type: string + password: + type: string + user: + type: string + type: object + provider: + type: string + type: object schedule: properties: date: From fb33b80a15ffdac826beec78985df3f2b6bf9099 Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Mon, 13 May 2024 10:20:04 -0700 Subject: [PATCH 21/26] remove proxy changes --- README.md | 4 - .../v1alpha1/managers/stage/stage-manager.go | 37 +--- api/pkg/apis/v1alpha1/model/campaign.go | 12 -- .../v1alpha1/providers/providerfactory.go | 15 -- .../v1alpha1/providers/stage/proxy/proxy.go | 160 ------------------ .../apis/v1alpha1/providers/stage/stage.go | 6 - api/pkg/apis/v1alpha1/utils/symphony-api.go | 19 --- .../apis/v1alpha1/vendors/processor-vendor.go | 98 ----------- .../apis/v1alpha1/vendors/vendorfactory.go | 2 - api/symphony-stage-runner.json | 149 ---------------- coa/pkg/apis/v1alpha2/events.go | 11 -- .../campaigns/stage-runner/activation.yaml | 9 - .../campaigns/stage-runner/campaign.yaml | 17 -- docs/symphony-book/images/stage-isolation.png | Bin 27519 -> 0 bytes docs/symphony-book/workflow/_overview.md | 2 - docs/symphony-book/workflow/provider-proxy.md | 38 ----- experimental-features.md | 12 -- k8s/apis/model/v1/common_types.go | 14 -- 18 files changed, 2 insertions(+), 603 deletions(-) delete mode 100644 api/pkg/apis/v1alpha1/providers/stage/proxy/proxy.go delete mode 100644 api/pkg/apis/v1alpha1/vendors/processor-vendor.go delete mode 100644 api/symphony-stage-runner.json delete mode 100644 docs/samples/campaigns/stage-runner/activation.yaml delete mode 100644 docs/samples/campaigns/stage-runner/campaign.yaml delete mode 100644 docs/symphony-book/images/stage-isolation.png delete mode 100644 docs/symphony-book/workflow/provider-proxy.md delete mode 100644 experimental-features.md diff --git a/README.md b/README.md index 97374fd06..611b60524 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,6 @@ _(last edit: 02/02/2024)_ -### ⚠️⚠️⚠️ This branch hosts experimental features. The Symphony community does not guarantee long-term support for these features. Their incorporation into the main branch is not assured, and they may be deprecated or removed at any time without notice. For more details, please see [Experimental Feature Table](./experimental-features.md)⚠️⚠️⚠️ - - - Symphony is a powerful service orchestration engine that enables the organization of multiple intelligent edge services into a seamless, end-to-end experience. Its primary purpose is to address the inherent complexity of edge deployment by providing a set of technology-agnostic workflow APIs, which are designed to deliver a streamlined experience for users across all device profiles. Symphony is uniquely capable of providing consistency across the entire software stack, from drivers to containers to configurations and policies. This comprehensive approach ensures that all aspects of your intelligent edge projects are effectively managed and optimized. Moreover, Symphony provides full support for the entire lifecycle of your edge computing initiatives, spanning from the initial deployment to ongoing updates and maintenance. diff --git a/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go b/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go index 3f7970a95..3d23a8112 100644 --- a/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go +++ b/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go @@ -239,7 +239,6 @@ func (s *StageManager) ResumeStage(status model.ActivationStatus, cam model.Camp TriggeringStage: stage, Schedule: cam.Stages[nextStage].Schedule, Namespace: namespace, - Proxy: cam.Stages[nextStage].Proxy, } log.Debugf(" M (Stage): Activating next stage: %s\n", activationData.Stage) return activationData, nil @@ -331,21 +330,7 @@ func (s *StageManager) HandleDirectTriggerEvent(ctx context.Context, triggerData } var outputs map[string]interface{} - if triggerData.Proxy != nil { - proxyProvider, err := factory.CreateProvider(triggerData.Proxy.Provider, nil) - if err != nil { - status.Status = v1alpha2.InternalError - status.ErrorMessage = err.Error() - status.IsActive = false - return status - } - if _, ok := proxyProvider.(contexts.IWithManagerContext); ok { - proxyProvider.(contexts.IWithManagerContext).SetContext(s.Manager.Context) - } - outputs, _, err = proxyProvider.(stage.IProxyStageProvider).Process(ctx, *s.Manager.Context, triggerData) - } else { - outputs, _, err = provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, triggerData.Inputs) - } + outputs, _, err = provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, triggerData.Inputs) result := TaskResult{ Outputs: outputs, @@ -587,23 +572,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca } else { var outputs map[string]interface{} var pause bool - if triggerData.Proxy != nil { - proxyProvider, err := factory.CreateProvider(triggerData.Proxy.Provider, nil) - if err != nil { - results <- TaskResult{ - Outputs: nil, - Error: err, - Site: site, - } - return - } - if _, ok := proxyProvider.(contexts.IWithManagerContext); ok { - proxyProvider.(contexts.IWithManagerContext).SetContext(s.Manager.Context) - } - outputs, pause, err = proxyProvider.(stage.IProxyStageProvider).Process(ctx, *s.Manager.Context, triggerData) - } else { - outputs, pause, err = provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, inputCopy) - } + outputs, pause, err = provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, inputCopy) if pause { pauseRequested = true } @@ -727,7 +696,6 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca TriggeringStage: triggerData.Stage, Schedule: nextStage.Schedule, Namespace: triggerData.Namespace, - Proxy: nextStage.Proxy, } } else { status.Status = v1alpha2.InternalError @@ -847,7 +815,6 @@ func (s *StageManager) HandleActivationEvent(ctx context.Context, actData v1alph TriggeringStage: stage, Schedule: stageSpec.Schedule, Namespace: actData.Namespace, - Proxy: stageSpec.Proxy, }, nil } return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("stage %s is not found", stage), v1alpha2.BadRequest) diff --git a/api/pkg/apis/v1alpha1/model/campaign.go b/api/pkg/apis/v1alpha1/model/campaign.go index 1b01bf4b1..94beeec4b 100644 --- a/api/pkg/apis/v1alpha1/model/campaign.go +++ b/api/pkg/apis/v1alpha1/model/campaign.go @@ -33,7 +33,6 @@ type StageSpec struct { Inputs map[string]interface{} `json:"inputs,omitempty"` HandleErrors bool `json:"handleErrors,omitempty"` Schedule *v1alpha2.ScheduleSpec `json:"schedule,omitempty"` - Proxy *v1alpha2.ProxySpec `json:"proxy,omitempty"` } func (s StageSpec) DeepEquals(other IDeepEquals) (bool, error) { @@ -65,17 +64,6 @@ func (s StageSpec) DeepEquals(other IDeepEquals) (bool, error) { if !reflect.DeepEqual(s.Schedule, otherS.Schedule) { return false, nil } - if s.Proxy == nil && otherS.Proxy != nil { - return false, nil - } - if s.Proxy != nil && otherS.Proxy == nil { - return false, nil - } - if s.Proxy != nil && otherS.Proxy != nil { - if !reflect.DeepEqual(s.Proxy.Provider, otherS.Proxy.Provider) { - return false, nil - } - } return true, nil } diff --git a/api/pkg/apis/v1alpha1/providers/providerfactory.go b/api/pkg/apis/v1alpha1/providers/providerfactory.go index 984d120ef..e59cb457a 100644 --- a/api/pkg/apis/v1alpha1/providers/providerfactory.go +++ b/api/pkg/apis/v1alpha1/providers/providerfactory.go @@ -20,7 +20,6 @@ import ( materialize "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/materialize" mockstage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/mock" patchstage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/patch" - proxystage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/proxy" remotestage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/remote" scriptstage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/script" waitstage "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/wait" @@ -164,12 +163,6 @@ func (s SymphonyProviderFactory) CreateProvider(providerType string, config cp.I if err == nil { return mProvider, nil } - case "providers.stage.proxy": - mProvider := &proxystage.ProxyStageProvider{} - err = mProvider.Init(config) - if err == nil { - return mProvider, nil - } case "providers.target.azure.iotedge": mProvider := &iotedge.IoTEdgeTargetProvider{} err = mProvider.Init(config) @@ -646,14 +639,6 @@ func CreateProviderForTargetRole(context *contexts.ManagerContext, role string, } provider.Context = context return provider, nil - case "providers.stage.proxy": - provider := &proxystage.ProxyStageProvider{} - err := provider.InitWithMap(binding.Config) - if err != nil { - return nil, err - } - provider.Context = context - return provider, nil case "providers.stage.counter": provider := &counterstage.CounterStageProvider{} err := provider.InitWithMap(binding.Config) diff --git a/api/pkg/apis/v1alpha1/providers/stage/proxy/proxy.go b/api/pkg/apis/v1alpha1/providers/stage/proxy/proxy.go deleted file mode 100644 index 6dc1be823..000000000 --- a/api/pkg/apis/v1alpha1/providers/stage/proxy/proxy.go +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * SPDX-License-Identifier: MIT - */ - -package proxy - -import ( - "context" - "encoding/json" - "sync" - - "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" - "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" - observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" - "github.com/eclipse-symphony/symphony/coa/pkg/logger" -) - -var msLock sync.Mutex -var sLog = logger.NewLogger("coa.runtime") - -type ProxyStageProviderConfig struct { - BaseUrl string `json:"baseUrl"` - User string `json:"user"` - Password string `json:"password"` -} - -type ProxyStageProvider struct { - Config ProxyStageProviderConfig - Context *contexts.ManagerContext -} - -func (s *ProxyStageProvider) Init(config providers.IProviderConfig) error { - msLock.Lock() - defer msLock.Unlock() - mockConfig, err := toProxyStageProviderConfig(config) - if err != nil { - return err - } - s.Config = mockConfig - return nil -} -func (s *ProxyStageProvider) SetContext(ctx *contexts.ManagerContext) { - s.Context = ctx -} -func toProxyStageProviderConfig(config providers.IProviderConfig) (ProxyStageProviderConfig, error) { - ret := ProxyStageProviderConfig{} - data, err := json.Marshal(config) - if err != nil { - return ret, err - } - err = json.Unmarshal(data, &ret) - return ret, err -} -func (i *ProxyStageProvider) InitWithMap(properties map[string]string) error { - config, err := SymphonyStageProviderConfigFromMap(properties) - if err != nil { - return err - } - return i.Init(config) -} -func SymphonyStageProviderConfigFromMap(properties map[string]string) (ProxyStageProviderConfig, error) { - ret := ProxyStageProviderConfig{} - baseUrl, err := utils.GetString(properties, "baseUrl") - if err != nil { - return ret, err - } - ret.BaseUrl = baseUrl - if ret.BaseUrl == "" { - return ret, v1alpha2.NewCOAError(nil, "baseUrl is required", v1alpha2.BadConfig) - } - user, err := utils.GetString(properties, "user") - if err != nil { - return ret, err - } - ret.User = user - if ret.User == "" { - return ret, v1alpha2.NewCOAError(nil, "user is required", v1alpha2.BadConfig) - } - password, err := utils.GetString(properties, "password") - if err != nil { - return ret, err - } - ret.Password = password - return ret, nil -} -func (m *ProxyStageProvider) traceValue(v interface{}, ctx interface{}) (interface{}, error) { - switch val := v.(type) { - case string: - parser := utils.NewParser(val) - context := m.Context.VencorContext.EvaluationContext.Clone() - context.Value = ctx - v, err := parser.Eval(*context) - if err != nil { - return "", err - } - switch vt := v.(type) { - case string: - return vt, nil - default: - return m.traceValue(v, ctx) - } - case []interface{}: - ret := []interface{}{} - for _, v := range val { - tv, err := m.traceValue(v, ctx) - if err != nil { - return "", err - } - ret = append(ret, tv) - } - return ret, nil - case map[string]interface{}: - ret := map[string]interface{}{} - for k, v := range val { - tv, err := m.traceValue(v, ctx) - if err != nil { - return "", err - } - ret[k] = tv - } - return ret, nil - default: - return val, nil - } -} - -func (i *ProxyStageProvider) Process(ctx context.Context, mgrContext contexts.ManagerContext, activationdata v1alpha2.ActivationData) (map[string]interface{}, bool, error) { - ctx, span := observability.StartSpan("[Stage] Proxy Provider", ctx, &map[string]string{ - "method": "Process", - }) - var err error = nil - var ret model.ActivationStatus - defer observ_utils.CloseSpanWithError(span, &err) - - sLog.Info(" P (Proxy Stage): start process request") - - ret, err = utils.CallRemoteProcessor(ctx, - activationdata.Proxy.Config.BaseUrl, - activationdata.Proxy.Config.User, - activationdata.Proxy.Config.Password, - activationdata) - if err != nil { - sLog.Errorf(" P (Proxy Stage): error calling remote stage processor %s", err.Error()) - return nil, false, err - } - if ret.ErrorMessage != "" { - sLog.Errorf(" P (Proxy Stage): remote stage processor returned an error %s", ret.ErrorMessage) - return nil, false, v1alpha2.NewCOAError(nil, ret.ErrorMessage, v1alpha2.InternalError) - } - outputs := ret.Outputs - - sLog.Info(" P (Proxy Stage): end process request") - return outputs, false, nil -} diff --git a/api/pkg/apis/v1alpha1/providers/stage/stage.go b/api/pkg/apis/v1alpha1/providers/stage/stage.go index 53ef862dd..95fe82cbd 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/stage.go +++ b/api/pkg/apis/v1alpha1/providers/stage/stage.go @@ -9,7 +9,6 @@ package stage import ( "context" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" ) @@ -18,11 +17,6 @@ type IStageProvider interface { Process(ctx context.Context, mgrContext contexts.ManagerContext, inputs map[string]interface{}) (map[string]interface{}, bool, error) } -type IProxyStageProvider interface { - // Return values: map[string]interface{} - outputs, bool - should the activation be paused (wait for a remote event), error - Process(ctx context.Context, mgrContext contexts.ManagerContext, activationdata v1alpha2.ActivationData) (map[string]interface{}, bool, error) -} - func ReadInputString(inputs map[string]interface{}, key string) string { if inputs == nil { return "" diff --git a/api/pkg/apis/v1alpha1/utils/symphony-api.go b/api/pkg/apis/v1alpha1/utils/symphony-api.go index 51f143851..e10d19911 100644 --- a/api/pkg/apis/v1alpha1/utils/symphony-api.go +++ b/api/pkg/apis/v1alpha1/utils/symphony-api.go @@ -250,25 +250,6 @@ func PublishActivationEvent(context context.Context, baseUrl string, user string return nil } -func CallRemoteProcessor(context context.Context, baseUrl string, user string, password string, event v1alpha2.ActivationData) (model.ActivationStatus, error) { - ret := model.ActivationStatus{} - token, err := auth(context, baseUrl, user, password) - - if err != nil { - return ret, err - } - event.Proxy = nil - jData, _ := json.Marshal(event) - response, err := callRestAPI(context, baseUrl, "processor", "POST", jData, token) - if err != nil { - return ret, err - } - err = json.Unmarshal(response, &ret) - if err != nil { - return ret, err - } - return ret, nil -} func GetABatchForSite(context context.Context, baseUrl string, site string, user string, password string) (model.SyncPackage, error) { ret := model.SyncPackage{} token, err := auth(context, baseUrl, user, password) diff --git a/api/pkg/apis/v1alpha1/vendors/processor-vendor.go b/api/pkg/apis/v1alpha1/vendors/processor-vendor.go deleted file mode 100644 index bfbf997f5..000000000 --- a/api/pkg/apis/v1alpha1/vendors/processor-vendor.go +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * SPDX-License-Identifier: MIT - */ - -package vendors - -import ( - "encoding/json" - - "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/stage" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" - observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" - "github.com/valyala/fasthttp" -) - -type ProcessorVendor struct { - vendors.Vendor - StageManager *stage.StageManager -} - -func (o *ProcessorVendor) GetInfo() vendors.VendorInfo { - return vendors.VendorInfo{ - Version: o.Vendor.Version, - Name: "Processor", - Producer: "Microsoft", - } -} - -func (e *ProcessorVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error { - err := e.Vendor.Init(config, factories, providers, pubsubProvider) - if err != nil { - return err - } - for _, m := range e.Managers { - if c, ok := m.(*stage.StageManager); ok { - e.StageManager = c - } - } - if e.StageManager == nil { - return v1alpha2.NewCOAError(nil, "stage manager is not supplied", v1alpha2.MissingConfig) - } - return nil -} - -func (o *ProcessorVendor) GetEndpoints() []v1alpha2.Endpoint { - route := "processor" - if o.Route != "" { - route = o.Route - } - return []v1alpha2.Endpoint{ - { - Methods: []string{fasthttp.MethodPost}, - Route: route, - Version: o.Version, - Handler: o.onProcess, - }, - } -} - -func (c *ProcessorVendor) onProcess(request v1alpha2.COARequest) v1alpha2.COAResponse { - ctx, span := observability.StartSpan("Processor Vendor", request.Context, &map[string]string{ - "method": "onProcess", - }) - defer span.End() - - switch request.Method { - case fasthttp.MethodPost: - triggerData := v1alpha2.ActivationData{} - err := json.Unmarshal(request.Body, &triggerData) - if err != nil { - tLog.Infof("V (Processor) : onProcess failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) - return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ - State: v1alpha2.InternalError, - Body: []byte(err.Error()), - }) - } - status := c.StageManager.HandleDirectTriggerEvent(ctx, triggerData) - jData, _ := json.Marshal(status) - return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ - State: v1alpha2.OK, - Body: jData, - }) - } - resp := v1alpha2.COAResponse{ - State: v1alpha2.MethodNotAllowed, - Body: []byte("{\"result\":\"405 - method not allowed\"}"), - ContentType: "application/json", - } - observ_utils.UpdateSpanStatusFromCOAResponse(span, resp) - return resp -} diff --git a/api/pkg/apis/v1alpha1/vendors/vendorfactory.go b/api/pkg/apis/v1alpha1/vendors/vendorfactory.go index 5f2b58fec..250490797 100644 --- a/api/pkg/apis/v1alpha1/vendors/vendorfactory.go +++ b/api/pkg/apis/v1alpha1/vendors/vendorfactory.go @@ -59,8 +59,6 @@ func (c SymphonyVendorFactory) CreateVendor(config vendors.VendorConfig) (vendor return &VisualizationClientVendor{}, nil case "vendors.visualization": return &VisualizationVendor{}, nil - case "vendors.processor": - return &ProcessorVendor{}, nil default: return nil, nil //Can't throw errors as other factories may create it... } diff --git a/api/symphony-stage-runner.json b/api/symphony-stage-runner.json deleted file mode 100644 index 5c2669915..000000000 --- a/api/symphony-stage-runner.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "siteInfo": { - "siteId": "symphony-proxy", - "currentSite": { - "baseUrl": "http://localhost:8098/v1alpha2/", - "username": "", - "password": "" - } - }, - "api": { - "pubsub": { - "shared": true, - "provider": { - "type": "providers.pubsub.memory", - "config": {} - } - }, - "vendors": [ - { - "type": "vendors.echo", - "route": "greetings", - "managers": [] - }, - { - "type": "vendors.users", - "loopInterval": 15, - "route": "users", - "properties": { - "test-users": "true" - }, - "managers": [ - { - "name": "users-manager", - "type": "managers.symphony.users", - "properties": { - "providers.state": "mem-state" - }, - "providers": { - "mem-state": { - "type": "providers.state.memory", - "config": {} - } - } - } - ] - }, - { - "type": "vendors.processor", - "route": "processor", - "managers": [ - { - "name": "processor-manager", - "type": "managers.symphony.stage", - "properties": { - "providers.state": "mem-state" - }, - "providers": { - "mem-state": { - "type": "providers.state.memory", - "config": {} - } - } - } - ] - } - ] - }, - "bindings": [ - { - "type": "bindings.http", - "config": { - "port": 9082, - "pipeline": [ - { - "type": "middleware.http.cors", - "properties": { - "Access-Control-Allow-Headers": "authorization,Content-Type", - "Access-Control-Allow-Credentials": "true", - "Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS", - "Access-Control-Allow-Origin": "*" - } - }, - { - "type": "middleware.http.jwt", - "properties": { - "ignorePaths": ["/v1alpha2/users/auth", "/v1alpha2/solution/instances", "/v1alpha2/agent/references", "/v1alpha2/greetings"], - "verifyKey": "SymphonyKey", - "enableRBAC": true, - "roles": [ - { - "role": "administrator", - "claim": "user", - "value": "admin" - }, - { - "role": "reader", - "claim": "user", - "value": "*" - }, - { - "role": "solution-creator", - "claim": "user", - "value": "developer" - }, - { - "role": "target-manager", - "claim": "user", - "value": "device-manager" - }, - { - "role": "operator", - "claim": "user", - "value": "solution-operator" - } - ], - "policy": { - "administrator": { - "items": { - "*": "*" - } - }, - "reader": { - "items": { - "*": "GET" - } - }, - "solution-creator": { - "items": { - "/v1alpha2/solutions": "*" - } - }, - "target-manager": { - "items": { - "/v1alpha2/targets": "*" - } - }, - "solution-operator": { - "items": { - "/v1alpha2/instances": "*" - } - } - } - } - } - ] - } - } - ] -} \ No newline at end of file diff --git a/coa/pkg/apis/v1alpha2/events.go b/coa/pkg/apis/v1alpha2/events.go index 858ffcea3..6302e6212 100644 --- a/coa/pkg/apis/v1alpha2/events.go +++ b/coa/pkg/apis/v1alpha2/events.go @@ -50,7 +50,6 @@ type ActivationData struct { TriggeringStage string `json:"triggeringStage,omitempty"` Schedule *ScheduleSpec `json:"schedule,omitempty"` NeedsReport bool `json:"needsReport,omitempty"` - Proxy *ProxySpec `json:"proxy,omitempty"` } type HeartBeatAction string @@ -72,16 +71,6 @@ type ScheduleSpec struct { Zone string `json:"zone"` } -type ProxyConfigSpec struct { - BaseUrl string `json:"baseUrl,omitempty"` - User string `json:"user,omitempty"` - Password string `json:"password,omitempty"` -} -type ProxySpec struct { - Provider string `json:"provider,omitempty"` - Config ProxyConfigSpec `json:"config,omitempty"` -} - func (s ScheduleSpec) ShouldFireNow() (bool, error) { dt, err := s.GetTime() if err != nil { diff --git a/docs/samples/campaigns/stage-runner/activation.yaml b/docs/samples/campaigns/stage-runner/activation.yaml deleted file mode 100644 index efda029d8..000000000 --- a/docs/samples/campaigns/stage-runner/activation.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: workflow.symphony/v1 -kind: Activation -metadata: - name: hello-world-runner-activation -spec: - campaign: "hello-world-runner" - name: "hello-world-runner-activation" - inputs: - foo: "bar" \ No newline at end of file diff --git a/docs/samples/campaigns/stage-runner/campaign.yaml b/docs/samples/campaigns/stage-runner/campaign.yaml deleted file mode 100644 index 668aca2b0..000000000 --- a/docs/samples/campaigns/stage-runner/campaign.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: workflow.symphony/v1 -kind: Campaign -metadata: - name: hello-world-runner -spec: - firstStage: "mock" - selfDriving: true - stages: - mock: - name: "mock" - provider: "providers.stage.mock" - proxy: - provider: providers.stage.proxy - config: - baseUrl: http://localhost:9082/v1alpha2/ - user: admin - password: "" \ No newline at end of file diff --git a/docs/symphony-book/images/stage-isolation.png b/docs/symphony-book/images/stage-isolation.png deleted file mode 100644 index 6fd18a345bdc81daeb1a553f7dd9b434d0139543..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27519 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 diff --git a/docs/symphony-book/workflow/_overview.md b/docs/symphony-book/workflow/_overview.md index f370a53a8..715e42fe6 100644 --- a/docs/symphony-book/workflow/_overview.md +++ b/docs/symphony-book/workflow/_overview.md @@ -3,7 +3,6 @@ Symphony has a built-in workflow engine that captures complex operational workflows into declarative models. In addition to basic workflow engine features like branches, conditions, loops and stateful stages, Symphony also supports a set of unique workflow features to enable large-scale, automated operations, including: * Remote execution of stages. * Scheduled execution. -* Isolated execution environment per workflow stage. * Fan-out execution of stages (map-reduce). ## Fundamentals @@ -23,7 +22,6 @@ Symphony has a built-in workflow engine that captures complex operational workfl * Scheduling * [Error handling and retries](./error-handling.md) -* [Stage isolation with provider proxy](./provider-proxy.md) * Remote execution * Fan-out execution diff --git a/docs/symphony-book/workflow/provider-proxy.md b/docs/symphony-book/workflow/provider-proxy.md deleted file mode 100644 index 1b6c08ea4..000000000 --- a/docs/symphony-book/workflow/provider-proxy.md +++ /dev/null @@ -1,38 +0,0 @@ -# Stage Isolation With Provider Proxy - -By default, all stage providers are invoked as in-proc calls on the Symphony control plane. This means all stage providers use the same service account context configured for the Symphony API. As the control plane often needs to manage a large number of resources, having a super user with access to all resources is an obvious security concern. A stage provider proxy allows a stage to be processed in an isolated environment such as a separate process, container, virtual machine, or physic device. The stage provider proxy expects a web server (called a **stage runner**) that implements the required Symphony stage provider interface. Although you can use your own stage runner implementations, we recommend using the default Symphony implementation that supports all existing Symphony stage providers to be used over the proxy. - -Such isolation has some distinct benefits: - -* Support different execution environments. As a stage runner can be hosted independently from the control plane, the stage runner can be configured with the exact toolchains for the specific stages. For example, a containerized stage runner can have all necessary tools pre-installed. Another example is that a Windows-based stage runner can use Windows toolchains. -* Because a stage runner runs in a different process, you can assign just enough access rights to the process to perform stage activities. -* The isolation also provides certain protection over vouge provider implementations, such as script-based attacks. -* Resources required by the runner can be mounted locally without needing to be shared with the control plane. - -The following diagram illustrates how stage isolation works with the provider proxy and a stage runner: - -![stage isolation](../images/stage-isolation.png) - -## Decarling stage proxy - -You can attach a proxy setting to any of the stage specs. For example, the follow stage spec specifies that the mock stage should be carried out remotely through a processor proxy: - -```yaml -stages: - mock: - name: mock - provider: providers.stage.mock - proxy: - provider: providers.stage.proxy - config: - baseUrl: http://localhost:9082/v1alpha2/ - user: admin - password: "" -``` - -## Launch a stage runner using Symphony API binary - -You can launch a stage runner by launching the `symphony-api` process with a `symphony-processor-server.json` config: -```bash -./symphony-api -c ./symphony-processor-server.json -l Debug -``` \ No newline at end of file diff --git a/experimental-features.md b/experimental-features.md deleted file mode 100644 index 8f8104c06..000000000 --- a/experimental-features.md +++ /dev/null @@ -1,12 +0,0 @@ -# Experimental Features - -|Area | Feature | Experimental | Main | -|--------|--------|--------|--------| -|Security| [Proxy Stage Provider](#proxy-stage-provider) | PR #229 | PR #230 | -|Security| Target provisioning| | | -|Security| Mutal cert authentication | | - - -* ### Proxy Stage Provider - - Proxy Stage Provider allows a Campaign stage to be executed in an isolated process. See [here](./docs/symphony-book/workflow/provider-proxy.md) for more details. \ No newline at end of file diff --git a/k8s/apis/model/v1/common_types.go b/k8s/apis/model/v1/common_types.go index b5bdbf178..f9b4f0d36 100644 --- a/k8s/apis/model/v1/common_types.go +++ b/k8s/apis/model/v1/common_types.go @@ -115,19 +115,6 @@ type ScheduleSpec struct { Zone string `json:"zone"` } -// +kubebuilder:object:generate=true -type ProxyConfigSpec struct { - BaseUrl string `json:"baseUrl,omitempty"` - User string `json:"user,omitempty"` - Password string `json:"password,omitempty"` -} - -// +kubebuilder:object:generate=true -type ProxySpec struct { - Provider string `json:"provider,omitempty"` - Config ProxyConfigSpec `json:"config,omitempty"` -} - // +kubebuilder:object:generate=true type StageSpec struct { Name string `json:"name,omitempty"` @@ -142,7 +129,6 @@ type StageSpec struct { Inputs runtime.RawExtension `json:"inputs,omitempty"` TriggeringStage string `json:"triggeringStage,omitempty"` Schedule *ScheduleSpec `json:"schedule,omitempty"` - Proxy *ProxySpec `json:"proxy,omitempty"` } // +kubebuilder:object:generate=true From 072b08a761e31e53fb98fe20d3472d0cc354eb31 Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Mon, 13 May 2024 10:24:20 -0700 Subject: [PATCH 22/26] re-generate artifacts --- k8s/apis/model/v1/zz_generated.deepcopy.go | 5 ----- .../oss/crd/bases/workflow.symphony_campaigns.yaml | 14 -------------- .../templates/symphony-core/symphonyk8s.yaml | 14 -------------- 3 files changed, 33 deletions(-) diff --git a/k8s/apis/model/v1/zz_generated.deepcopy.go b/k8s/apis/model/v1/zz_generated.deepcopy.go index 6c9657993..73a91b538 100644 --- a/k8s/apis/model/v1/zz_generated.deepcopy.go +++ b/k8s/apis/model/v1/zz_generated.deepcopy.go @@ -225,11 +225,6 @@ func (in *StageSpec) DeepCopyInto(out *StageSpec) { *out = new(ScheduleSpec) **out = **in } - if in.Proxy != nil { - in, out := &in.Proxy, &out.Proxy - *out = new(ProxySpec) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StageSpec. diff --git a/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml b/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml index 31c7a52fe..28d8821a9 100644 --- a/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml +++ b/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml @@ -53,20 +53,6 @@ spec: type: string provider: type: string - proxy: - properties: - config: - properties: - baseUrl: - type: string - password: - type: string - user: - type: string - type: object - provider: - type: string - type: object schedule: properties: date: diff --git a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index fa9a3ef6c..34e8f4c6f 100644 --- a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml @@ -154,20 +154,6 @@ spec: type: string provider: type: string - proxy: - properties: - config: - properties: - baseUrl: - type: string - password: - type: string - user: - type: string - type: object - provider: - type: string - type: object schedule: properties: date: From 7f078cd27568a918958b9eb971e946311647e4f0 Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Mon, 13 May 2024 10:28:41 -0700 Subject: [PATCH 23/26] remove version.txt from branch --- .github/version/versions.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/version/versions.txt diff --git a/.github/version/versions.txt b/.github/version/versions.txt deleted file mode 100644 index ddc1aee2c..000000000 --- a/.github/version/versions.txt +++ /dev/null @@ -1 +0,0 @@ -0.48.23 \ No newline at end of file From 164238bb2a620e242a301513bdbbe1d9033794aa Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Mon, 13 May 2024 10:32:22 -0700 Subject: [PATCH 24/26] restore version.txt from main branch --- .github/version/versions.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/version/versions.txt diff --git a/.github/version/versions.txt b/.github/version/versions.txt new file mode 100644 index 000000000..3803b3e1a --- /dev/null +++ b/.github/version/versions.txt @@ -0,0 +1 @@ +0.48.23 From b1a9cfefa9c88b1af87eb6d34fa004fd6a829232 Mon Sep 17 00:00:00 2001 From: Jiawei Du <59427055+msftcoderdjw@users.noreply.github.com> Date: Tue, 14 May 2024 09:46:19 +0800 Subject: [PATCH 25/26] add statusMessage to show user-friendly display (#255) --- .../activations/activations-manager_test.go | 6 +++- .../v1alpha1/managers/stage/stage-manager.go | 36 +++++++++++++++---- .../managers/stage/stage-manager_test.go | 29 +++++++++++---- api/pkg/apis/v1alpha1/model/campaign.go | 1 + .../vendors/activations-vendor_test.go | 6 ++-- .../vendors/federation-vendor_test.go | 1 + api/pkg/apis/v1alpha1/vendors/stage-vendor.go | 17 +++++---- .../v1alpha1/vendors/stage-vendor_test.go | 2 +- coa/pkg/apis/v1alpha2/types.go | 4 +++ docs/samples/opera/app/types.d.ts | 1 + .../components/campaigns/CampaignCard.tsx | 2 +- k8s/apis/workflow/v1/activation_types.go | 3 +- .../bases/workflow.symphony_activations.yaml | 4 ++- .../templates/symphony-core/symphonyk8s.yaml | 4 ++- 14 files changed, 90 insertions(+), 26 deletions(-) diff --git a/api/pkg/apis/v1alpha1/managers/activations/activations-manager_test.go b/api/pkg/apis/v1alpha1/managers/activations/activations-manager_test.go index 5fb8ba5a0..bceb8ccc7 100644 --- a/api/pkg/apis/v1alpha1/managers/activations/activations-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/activations/activations-manager_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" "github.com/stretchr/testify/assert" ) @@ -46,7 +47,10 @@ func TestCleanupOldActivationSpec(t *testing.T) { spec, err := manager.GetState(context.Background(), "test", "default") assert.Nil(t, err) assert.Equal(t, "test", spec.ObjectMeta.Name) - err = manager.ReportStatus(context.Background(), "test", "default", model.ActivationStatus{Status: 9996}) + err = manager.ReportStatus(context.Background(), "test", "default", model.ActivationStatus{ + Status: v1alpha2.Done, + StatusMessage: v1alpha2.Done.String(), + }) assert.Nil(t, err) errList := cleanupmanager.Poll() assert.Empty(t, errList) diff --git a/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go b/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go index 3d23a8112..33b06b2ef 100644 --- a/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go +++ b/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go @@ -287,21 +287,24 @@ func (s *StageManager) HandleDirectTriggerEvent(ctx context.Context, triggerData "__stage": triggerData.Stage, "__site": s.VendorContext.SiteInfo.SiteId, }, - Status: v1alpha2.Untouched, - ErrorMessage: "", - IsActive: true, + Status: v1alpha2.Untouched, + StatusMessage: v1alpha2.Untouched.String(), + ErrorMessage: "", + IsActive: true, } var provider providers.IProvider factory := symproviders.SymphonyProviderFactory{} provider, err = factory.CreateProvider(triggerData.Provider, triggerData.Config) if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = err.Error() status.IsActive = false return status } if provider == nil { status.Status = v1alpha2.BadRequest + status.StatusMessage = v1alpha2.BadRequest.String() status.ErrorMessage = fmt.Sprintf("provider %s is not found", triggerData.Provider) status.IsActive = false return status @@ -325,6 +328,7 @@ func (s *StageManager) HandleDirectTriggerEvent(ctx context.Context, triggerData }) status.Outputs["__status"] = v1alpha2.Delayed status.Status = v1alpha2.Paused + status.StatusMessage = v1alpha2.Paused.String() status.IsActive = false return status } @@ -341,6 +345,7 @@ func (s *StageManager) HandleDirectTriggerEvent(ctx context.Context, triggerData err = result.GetError() if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = err.Error() status.IsActive = false status.Outputs = carryOutPutsToErrorStatus(outputs, err, "") @@ -355,6 +360,7 @@ func (s *StageManager) HandleDirectTriggerEvent(ctx context.Context, triggerData status.Outputs["__status"] = v1alpha2.OK status.Status = v1alpha2.Done + status.StatusMessage = v1alpha2.Done.String() status.IsActive = false return status @@ -403,9 +409,10 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca "__stage": triggerData.Stage, "__site": s.VendorContext.SiteInfo.SiteId, }, - Status: v1alpha2.Untouched, - ErrorMessage: "", - IsActive: true, + Status: v1alpha2.Untouched, + StatusMessage: v1alpha2.Untouched.String(), + ErrorMessage: "", + IsActive: true, } var activationData *v1alpha2.ActivationData if currentStage, ok := campaign.Stages[triggerData.Stage]; ok { @@ -426,6 +433,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca val, err = parser.Eval(*eCtx) if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = err.Error() status.IsActive = false log.Errorf(" M (Stage): failed to evaluate context: %v", err) @@ -441,6 +449,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca sites = append(sites, val.(string)) } else { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = fmt.Sprintf("invalid context %s", currentStage.Contexts) status.IsActive = false log.Errorf(" M (Stage): invalid context: %v", currentStage.Contexts) @@ -480,6 +489,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca val, err = s.traceValue(v, inputs, triggerData.Outputs) if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = err.Error() status.IsActive = false log.Errorf(" M (Stage): failed to evaluate input: %v", err) @@ -503,6 +513,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca provider, err = factory.CreateProvider(triggerData.Provider, triggerData.Config) if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = err.Error() status.IsActive = false log.Errorf(" M (Stage): failed to create provider: %v", err) @@ -510,6 +521,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca } if provider == nil { status.Status = v1alpha2.BadRequest + status.StatusMessage = v1alpha2.BadRequest.String() status.ErrorMessage = fmt.Sprintf("provider %s is not found", triggerData.Provider) status.IsActive = false log.Errorf(" M (Stage): failed to create provider: %v", err) @@ -542,6 +554,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca val, err = s.traceValue(v, inputCopy, triggerData.Outputs) if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = err.Error() status.IsActive = false log.Errorf(" M (Stage): failed to evaluate input: %v", err) @@ -594,6 +607,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca err = result.GetError() if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = fmt.Sprintf("%s: %s", result.Site, err.Error()) status.IsActive = false site := result.Site @@ -648,12 +662,14 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca }) if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = err.Error() status.IsActive = false log.Errorf(" M (Stage): failed to save pending task: %v", err) return status, activationData } status.Status = v1alpha2.Paused + status.StatusMessage = v1alpha2.Paused.String() status.IsActive = false return status, activationData } @@ -671,6 +687,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca val, err = parser.Eval(*eCtx) if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = err.Error() status.IsActive = false log.Errorf(" M (Stage): failed to evaluate stage selector: %v", err) @@ -699,6 +716,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca } } else { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = fmt.Sprintf("stage %s failed", triggerData.Stage) status.IsActive = false log.Errorf(" M (Stage): failed to process stage outputs: %v", status.ErrorMessage) @@ -707,6 +725,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca } else { err = v1alpha2.NewCOAError(nil, status.ErrorMessage, v1alpha2.BadRequest) status.Status = v1alpha2.BadRequest + status.StatusMessage = v1alpha2.BadRequest.String() status.ErrorMessage = fmt.Sprintf("stage %s is not found", sVal) status.IsActive = false log.Errorf(" M (Stage): failed to find next stage: %v", err) @@ -717,19 +736,23 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca if sVal == "" { status.IsActive = false status.Status = v1alpha2.Done + status.StatusMessage = v1alpha2.Done.String() } else { if pauseRequested { status.IsActive = false status.Status = v1alpha2.Paused + status.StatusMessage = v1alpha2.Paused.String() } else { status.IsActive = true status.Status = v1alpha2.Running + status.StatusMessage = v1alpha2.Running.String() } } log.Infof(" M (Stage): stage %s is done", triggerData.Stage) return status, activationData } else { status.Status = v1alpha2.Done + status.StatusMessage = v1alpha2.Done.String() status.NextStage = "" status.IsActive = false log.Infof(" M (Stage): stage %s is done (no next stage)", triggerData.Stage) @@ -738,6 +761,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca } err = v1alpha2.NewCOAError(nil, fmt.Sprintf("stage %s is not found", triggerData.Stage), v1alpha2.BadRequest) status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.ErrorMessage = err.Error() status.IsActive = false log.Errorf(" M (Stage): failed to find stage: %v", err) diff --git a/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go b/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go index 1638576f3..cb7c8ff6b 100644 --- a/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go @@ -73,6 +73,7 @@ func TestCampaignWithSingleMockStageLoop(t *testing.T) { } } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) assert.Equal(t, int64(5), status.Outputs["foo"]) assert.Equal(t, "fakens", status.Outputs["__namespace"]) assert.Equal(t, "test-campaign", status.Outputs["__campaign"]) @@ -129,6 +130,7 @@ func TestCampaignWithSingleCounterStageLoop(t *testing.T) { } } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) assert.Equal(t, int64(5), status.Outputs["foo"]) assert.Equal(t, "fakens", status.Outputs["__namespace"]) assert.Equal(t, "test-campaign", status.Outputs["__campaign"]) @@ -186,6 +188,7 @@ func TestCampaignWithSingleMegativeCounterStageLoop(t *testing.T) { } } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) assert.Equal(t, int64(-50), status.Outputs["foo"]) assert.Equal(t, "fakens", status.Outputs["__namespace"]) assert.Equal(t, "test-campaign", status.Outputs["__campaign"]) @@ -248,6 +251,7 @@ func TestCampaignWithTwoCounterStageLoop(t *testing.T) { } } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) assert.Equal(t, int64(5), status.Outputs["foo"]) assert.Equal(t, int64(5), status.Outputs["bar"]) assert.Equal(t, "fakens", status.Outputs["__namespace"]) @@ -314,6 +318,7 @@ func TestCampaignWithHTTPCounterStageLoop(t *testing.T) { } } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) assert.Equal(t, int64(5), status.Outputs["success"]) assert.Equal(t, "fakens", status.Outputs["__namespace"]) assert.Equal(t, "test-campaign", status.Outputs["__campaign"]) @@ -370,6 +375,7 @@ func TestCampaignWithDelay(t *testing.T) { } } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) assert.Equal(t, v1alpha2.OK, status.Outputs[v1alpha2.StatusOutput]) assert.True(t, time.Now().UTC().Sub(timeStamp) > 5*time.Second) assert.Equal(t, "fakens", status.Outputs["__namespace"]) @@ -436,6 +442,7 @@ func TestErrorHandler(t *testing.T) { } } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) assert.Equal(t, int64(0), status.Outputs["success"]) assert.Equal(t, "fakens", status.Outputs["__namespace"]) assert.Equal(t, "test-campaign", status.Outputs["__campaign"]) @@ -499,6 +506,7 @@ func TestErrorHandlerNotSet(t *testing.T) { } } assert.Equal(t, v1alpha2.InternalError, status.Status) + assert.True(t, v1alpha2.InternalError.EqualsWithString(status.StatusMessage)) } func TestAccessingPreviousStage(t *testing.T) { stateProvider := &memorystate.MemoryStateProvider{} @@ -661,6 +669,7 @@ func TestIntentionalError(t *testing.T) { assert.Equal(t, v1alpha2.BadRequest, status.Outputs["__status"]) } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) } func TestIntentionalErrorState(t *testing.T) { stateProvider := &memorystate.MemoryStateProvider{} @@ -715,6 +724,7 @@ func TestIntentionalErrorState(t *testing.T) { assert.Equal(t, v1alpha2.DeleteFailed, status.Outputs["__status"]) } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) } func TestIntentionalErrorString(t *testing.T) { stateProvider := &memorystate.MemoryStateProvider{} @@ -768,6 +778,7 @@ func TestIntentionalErrorString(t *testing.T) { assert.Equal(t, v1alpha2.InternalError, status.Outputs["__status"]) // non-successful state is returned without __error, set to InternalError } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) } func TestIntentionalErrorStringProper(t *testing.T) { stateProvider := &memorystate.MemoryStateProvider{} @@ -823,6 +834,7 @@ func TestIntentionalErrorStringProper(t *testing.T) { assert.Equal(t, "Bad Request: this_is_an_error", status.Outputs["__error"]) } assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) } func TestAccessingPreviousStageInExpression(t *testing.T) { stateProvider := &memorystate.MemoryStateProvider{} @@ -920,9 +932,10 @@ func TestResumeStage(t *testing.T) { }, }) activation := model.ActivationStatus{ - Status: v1alpha2.Done, - Stage: "test", - Outputs: output, + Status: v1alpha2.Done, + StatusMessage: v1alpha2.Done.String(), + Stage: "test", + Outputs: output, } campaign := model.CampaignSpec{ SelfDriving: true, @@ -985,9 +998,10 @@ func TestResumeStageFailed(t *testing.T) { }, }) activation := model.ActivationStatus{ - Status: v1alpha2.Done, - Stage: "test", - Outputs: output, + Status: v1alpha2.Done, + StatusMessage: v1alpha2.Done.String(), + Stage: "test", + Outputs: output, } campaign := model.CampaignSpec{ SelfDriving: true, @@ -1050,6 +1064,7 @@ func TestHandleDirectTriggerEvent(t *testing.T) { } status := manager.HandleDirectTriggerEvent(context.Background(), activation) assert.Equal(t, v1alpha2.Done, status.Status) + assert.True(t, v1alpha2.Done.EqualsWithString(status.StatusMessage)) assert.Equal(t, "test-campaign", status.Outputs["__campaign"]) assert.Equal(t, "test-activation", status.Outputs["__activation"]) assert.Equal(t, "1", status.Outputs["__activationGeneration"]) @@ -1097,6 +1112,7 @@ func TestHandleDirectTriggerScheduleEvent(t *testing.T) { } status := manager.HandleDirectTriggerEvent(context.Background(), activation) assert.Equal(t, v1alpha2.Paused, status.Status) + assert.True(t, v1alpha2.Paused.EqualsWithString(status.StatusMessage)) assert.Equal(t, v1alpha2.Delayed, status.Outputs["__status"]) assert.Equal(t, false, status.IsActive) @@ -1220,5 +1236,6 @@ func TestTriggerEventWithSchedule(t *testing.T) { }, }, *activation) assert.Equal(t, v1alpha2.Paused, status.Status) + assert.True(t, v1alpha2.Paused.EqualsWithString(status.StatusMessage)) assert.Equal(t, false, status.IsActive) } diff --git a/api/pkg/apis/v1alpha1/model/campaign.go b/api/pkg/apis/v1alpha1/model/campaign.go index 94beeec4b..c534ec6a7 100644 --- a/api/pkg/apis/v1alpha1/model/campaign.go +++ b/api/pkg/apis/v1alpha1/model/campaign.go @@ -73,6 +73,7 @@ type ActivationStatus struct { Inputs map[string]interface{} `json:"inputs,omitempty"` Outputs map[string]interface{} `json:"outputs,omitempty"` Status v1alpha2.State `json:"status,omitempty"` + StatusMessage string `json:"statusMessage,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` IsActive bool `json:"isActive,omitempty"` ActivationGeneration string `json:"activationGeneration,omitempty"` diff --git a/api/pkg/apis/v1alpha1/vendors/activations-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/activations-vendor_test.go index 980c229e9..e95627545 100644 --- a/api/pkg/apis/v1alpha1/vendors/activations-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/activations-vendor_test.go @@ -48,7 +48,8 @@ func TestActivationsInfo(t *testing.T) { func TestActivationsOnStatus(t *testing.T) { vendor := createActivationsVendor() status := model.ActivationStatus{ - Status: 9996, + Status: v1alpha2.Done, + StatusMessage: v1alpha2.Done.String(), } data, _ := json.Marshal(status) resp := vendor.onStatus(v1alpha2.COARequest{ @@ -149,7 +150,8 @@ func TestActivationsOnActivations(t *testing.T) { assert.Equal(t, campaignName, activations[0].Spec.Campaign) status := model.ActivationStatus{ - Status: 9996, + Status: v1alpha2.Done, + StatusMessage: v1alpha2.Done.String(), } data, _ = json.Marshal(status) resp = vendor.onStatus(v1alpha2.COARequest{ diff --git a/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go index f80558b26..0bad17500 100644 --- a/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/federation-vendor_test.go @@ -243,6 +243,7 @@ func TestFederationOnSyncPost(t *testing.T) { "output2": "value2", }, Status: v1alpha2.OK, + StatusMessage: v1alpha2.OK.String(), IsActive: true, ActivationGeneration: "1", UpdateTime: "exampleUpdateTime", diff --git a/api/pkg/apis/v1alpha1/vendors/stage-vendor.go b/api/pkg/apis/v1alpha1/vendors/stage-vendor.go index a630d656f..92e517633 100644 --- a/api/pkg/apis/v1alpha1/vendors/stage-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/stage-vendor.go @@ -106,12 +106,13 @@ func (s *StageVendor) Init(config vendors.VendorConfig, factories []managers.IMa s.Vendor.Context.Subscribe("trigger", func(topic string, event v1alpha2.Event) error { log.Info("V (Stage): handling trigger event") status := model.ActivationStatus{ - Stage: "", - NextStage: "", - Outputs: map[string]interface{}{}, - Status: v1alpha2.Untouched, - ErrorMessage: "", - IsActive: true, + Stage: "", + NextStage: "", + Outputs: map[string]interface{}{}, + Status: v1alpha2.Untouched, + StatusMessage: v1alpha2.Untouched.String(), + ErrorMessage: "", + IsActive: true, } triggerData := v1alpha2.ActivationData{} jData, _ := json.Marshal(event.Body) @@ -119,6 +120,7 @@ func (s *StageVendor) Init(config vendors.VendorConfig, factories []managers.IMa if err != nil { err = v1alpha2.NewCOAError(nil, "event body is not an activation job", v1alpha2.BadRequest) status.Status = v1alpha2.BadRequest + status.StatusMessage = v1alpha2.BadRequest.String() status.ErrorMessage = err.Error() status.IsActive = false sLog.Errorf("V (Stage): failed to deserialize activation data: %v", err) @@ -132,6 +134,7 @@ func (s *StageVendor) Init(config vendors.VendorConfig, factories []managers.IMa campaign, err := s.CampaignsManager.GetState(context.TODO(), triggerData.Campaign, triggerData.Namespace) if err != nil { status.Status = v1alpha2.BadRequest + status.StatusMessage = v1alpha2.BadRequest.String() status.ErrorMessage = err.Error() status.IsActive = false sLog.Errorf("V (Stage): failed to get campaign spec: %v", err) @@ -144,6 +147,7 @@ func (s *StageVendor) Init(config vendors.VendorConfig, factories []managers.IMa status.ActivationGeneration = triggerData.ActivationGeneration status.ErrorMessage = "" status.Status = v1alpha2.Running + status.StatusMessage = v1alpha2.Running.String() if triggerData.NeedsReport { sLog.Debugf("V (Stage): reporting status: %v", status) s.Vendor.Context.Publish("report", v1alpha2.Event{ @@ -210,6 +214,7 @@ func (s *StageVendor) Init(config vendors.VendorConfig, factories []managers.IMa activation, err := s.StageManager.ResumeStage(status, *campaign.Spec) if err != nil { status.Status = v1alpha2.InternalError + status.StatusMessage = v1alpha2.InternalError.String() status.IsActive = false status.ErrorMessage = fmt.Sprintf("failed to resume stage: %v", err) sLog.Errorf("V (Stage): failed to resume stage: %v", err) diff --git a/api/pkg/apis/v1alpha1/vendors/stage-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/stage-vendor_test.go index b75fdaa90..98f7c9e8c 100644 --- a/api/pkg/apis/v1alpha1/vendors/stage-vendor_test.go +++ b/api/pkg/apis/v1alpha1/vendors/stage-vendor_test.go @@ -196,5 +196,5 @@ func createStageVendor() StageVendor { // assert.NotNil(t, activation) // assert.Equal(t, "test-activation", activation.Id) // assert.NotNil(t, activation.Status.UpdateTime) -// assert.Equal(t, v1alpha2.Done, activation.Status.Status) +// assert.True(t, v1alpha2.Done.EqualsWithString(activation.Status.Status)) // } diff --git a/coa/pkg/apis/v1alpha2/types.go b/coa/pkg/apis/v1alpha2/types.go index ff6e87be8..2c0ff0648 100644 --- a/coa/pkg/apis/v1alpha2/types.go +++ b/coa/pkg/apis/v1alpha2/types.go @@ -99,6 +99,10 @@ const ( TargetPropertyNotFound State = 12000 ) +func (s State) EqualsWithString(str string) bool { + return s.String() == str +} + func (s State) String() string { switch s { case OK: diff --git a/docs/samples/opera/app/types.d.ts b/docs/samples/opera/app/types.d.ts index a0e65c933..178e9c818 100644 --- a/docs/samples/opera/app/types.d.ts +++ b/docs/samples/opera/app/types.d.ts @@ -214,6 +214,7 @@ export interface ActivationStatus { inputs: Record; outputs: Record; status: number; + statusMessage: string; errorMessage: string; isActive: boolean; activationGeneration: string; diff --git a/docs/samples/opera/components/campaigns/CampaignCard.tsx b/docs/samples/opera/components/campaigns/CampaignCard.tsx index 86dc3fb13..4970eb78d 100644 --- a/docs/samples/opera/components/campaigns/CampaignCard.tsx +++ b/docs/samples/opera/components/campaigns/CampaignCard.tsx @@ -85,7 +85,7 @@ function CampaignCard(props: CampaignCardProps) { {activation && ( -

{` ${stateToString(activation.status.status)} ${activation.status.stage === '' ? '': 'stage (' + activation.status.stage + ')'}`}
+
{` ${activation.status.statusMessage} ${activation.status.stage === '' ? '': 'stage (' + activation.status.stage + ')'}`}
)} diff --git a/k8s/apis/workflow/v1/activation_types.go b/k8s/apis/workflow/v1/activation_types.go index f4bcb0ef6..8a0459e28 100644 --- a/k8s/apis/workflow/v1/activation_types.go +++ b/k8s/apis/workflow/v1/activation_types.go @@ -24,6 +24,7 @@ type ActivationStatus struct { // +kubebuilder:validation:Schemaless Outputs runtime.RawExtension `json:"outputs,omitempty"` Status v1alpha2.State `json:"status,omitempty"` + StatusMessage string `json:"statusMessage,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` IsActive bool `json:"isActive,omitempty"` ActivationGeneration string `json:"activationGeneration,omitempty"` @@ -33,7 +34,7 @@ type ActivationStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Next Stage",type=string,JSONPath=`.status.nextStage` -// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.statusMessage` // Activation is the Schema for the activations API type Activation struct { metav1.TypeMeta `json:",inline"` diff --git a/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml b/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml index c1bf2e7e6..070e9793b 100644 --- a/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml +++ b/k8s/config/oss/crd/bases/workflow.symphony_activations.yaml @@ -19,7 +19,7 @@ spec: - jsonPath: .status.nextStage name: Next Stage type: string - - jsonPath: .status.status + - jsonPath: .status.statusMessage name: Status type: string name: v1 @@ -68,6 +68,8 @@ spec: type: string status: type: integer + statusMessage: + type: string updateTime: type: string required: diff --git a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index 34e8f4c6f..e36ce5528 100644 --- a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml @@ -29,7 +29,7 @@ spec: - jsonPath: .status.nextStage name: Next Stage type: string - - jsonPath: .status.status + - jsonPath: .status.statusMessage name: Status type: string name: v1 @@ -78,6 +78,8 @@ spec: type: string status: type: integer + statusMessage: + type: string updateTime: type: string required: From fdb1ac67ea3dbaa5ad6b95710d252e203ef790ce Mon Sep 17 00:00:00 2001 From: Haishi Bai Date: Wed, 15 May 2024 12:48:33 -0700 Subject: [PATCH 26/26] merge conflict --- k8s/apis/model/v1/zz_generated.deepcopy.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/k8s/apis/model/v1/zz_generated.deepcopy.go b/k8s/apis/model/v1/zz_generated.deepcopy.go index adb77d382..ba92cf76d 100644 --- a/k8s/apis/model/v1/zz_generated.deepcopy.go +++ b/k8s/apis/model/v1/zz_generated.deepcopy.go @@ -125,18 +125,6 @@ func (in *ComponentSpec) DeepCopy() *ComponentSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -<<<<<<< HEAD -func (in *ProxyConfigSpec) DeepCopyInto(out *ProxyConfigSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyConfigSpec. -func (in *ProxyConfigSpec) DeepCopy() *ProxyConfigSpec { - if in == nil { - return nil - } - out := new(ProxyConfigSpec) -======= func (in *DeployableStatus) DeepCopyInto(out *DeployableStatus) { *out = *in if in.Properties != nil { @@ -156,7 +144,6 @@ func (in *DeployableStatus) DeepCopy() *DeployableStatus { return nil } out := new(DeployableStatus) ->>>>>>> main in.DeepCopyInto(out) return out }