From 9ffb2eeaeae8cabc3f481aeddf10af01d77db450 Mon Sep 17 00:00:00 2001 From: shanewxy <592491808@qq.com> Date: Tue, 16 Apr 2024 22:42:55 -0700 Subject: [PATCH] refactor: connector --- go.mod | 41 ++- go.sum | 112 ++++++- pkg/apistatus/connector_status.go | 32 ++ pkg/controllers/setup.go | 1 + .../walrus/project_subject.authz.go | 3 +- pkg/controllers/walruscore/connector.go | 113 ++++++- .../walruscore/connector_binding.go | 160 ++++++++++ pkg/extensionapis/setup.go | 1 + pkg/extensionapis/walrus/connector.go | 55 +++- pkg/extensionapis/walrus/connector_binding.go | 125 ++++++++ pkg/extensionapis/walrus/environment.go | 3 +- pkg/resourcehandler/consts.go | 10 + pkg/resourcehandler/types.go | 107 +++++++ pkg/resourcehandlers/alibaba/key/key.go | 26 ++ .../alibaba/resourceexec/common.go | 3 + .../alibaba/resourceexec/ecs_instance.go | 280 ++++++++++++++++++ .../alibaba/resourceexec/registry.go | 83 ++++++ .../alibaba/resourcehandler.go | 112 +++++++ .../alibaba/resourcelog/client.go | 26 ++ .../alibaba/resourcelog/common.go | 3 + .../alibaba/resourcelog/ecs_instance.go | 91 ++++++ .../alibaba/resourcelog/registry.go | 70 +++++ pkg/resourcehandlers/aws/key/key.go | 26 ++ .../aws/resourceexec/ec2_instance.go | 142 +++++++++ .../aws/resourceexec/registry.go | 83 ++++++ pkg/resourcehandlers/aws/resourcehandler.go | 109 +++++++ .../aws/resourcelog/client.go | 18 ++ .../aws/resourcelog/ec2_instance.go | 89 ++++++ .../aws/resourcelog/registry.go | 69 +++++ pkg/resourcehandlers/aws/types/credential.go | 49 +++ pkg/resourcehandlers/azure/resourcehandler.go | 108 +++++++ .../azure/types/credential.go | 45 +++ .../docker/resourcehandler.go | 96 ++++++ .../google/resourcehandler.go | 98 ++++++ .../google/types/credential.go | 45 +++ pkg/resourcehandlers/k8s/driver.go | 101 +++++++ pkg/resourcehandlers/k8s/helper.go | 33 +++ .../k8s/intercept/converter_tf.go | 171 +++++++++++ .../k8s/intercept/enforcer_accessible.go | 51 ++++ .../k8s/intercept/enforcer_composite.go | 64 ++++ .../k8s/intercept/enforcer_operable.go | 48 +++ .../k8s/intercept/tf_types.go | 111 +++++++ pkg/resourcehandlers/k8s/intercept/types.go | 26 ++ pkg/resourcehandlers/k8s/key/codec.go | 30 ++ .../k8s/kube/namspaced_name.go | 27 ++ pkg/resourcehandlers/k8s/kube/pod.go | 256 ++++++++++++++++ pkg/resourcehandlers/k8s/kubelabel/labels.go | 150 ++++++++++ .../k8s/polymorphic/selectors.go | 57 ++++ .../k8s/polymorphic/serializer.go | 43 +++ pkg/resourcehandlers/k8s/resourcehandler.go | 139 +++++++++ pkg/resourcehandlers/registry.go | 48 +++ pkg/resourcehandlers/types/credentials.go | 66 +++++ pkg/resourcehandlers/types/util.go | 31 ++ pkg/resourcehandlers/types/wrap_resource.go | 16 + .../unknown/resourcehandler.go | 66 +++++ pkg/resourcehandlers/validate.go | 35 +++ pkg/systemkuberes/connector.go | 148 +++++++++ pkg/systemkuberes/environment.go | 73 ++++- pkg/webhooks/setup.go | 1 + pkg/webhooks/walruscore/connector.go | 125 +++++++- pkg/webhooks/walruscore/connector_binding.go | 144 +++++++++ 61 files changed, 4466 insertions(+), 28 deletions(-) create mode 100644 pkg/apistatus/connector_status.go create mode 100644 pkg/controllers/walruscore/connector_binding.go create mode 100644 pkg/extensionapis/walrus/connector_binding.go create mode 100644 pkg/resourcehandler/consts.go create mode 100644 pkg/resourcehandler/types.go create mode 100644 pkg/resourcehandlers/alibaba/key/key.go create mode 100644 pkg/resourcehandlers/alibaba/resourceexec/common.go create mode 100644 pkg/resourcehandlers/alibaba/resourceexec/ecs_instance.go create mode 100644 pkg/resourcehandlers/alibaba/resourceexec/registry.go create mode 100644 pkg/resourcehandlers/alibaba/resourcehandler.go create mode 100644 pkg/resourcehandlers/alibaba/resourcelog/client.go create mode 100644 pkg/resourcehandlers/alibaba/resourcelog/common.go create mode 100644 pkg/resourcehandlers/alibaba/resourcelog/ecs_instance.go create mode 100644 pkg/resourcehandlers/alibaba/resourcelog/registry.go create mode 100644 pkg/resourcehandlers/aws/key/key.go create mode 100644 pkg/resourcehandlers/aws/resourceexec/ec2_instance.go create mode 100644 pkg/resourcehandlers/aws/resourceexec/registry.go create mode 100644 pkg/resourcehandlers/aws/resourcehandler.go create mode 100644 pkg/resourcehandlers/aws/resourcelog/client.go create mode 100644 pkg/resourcehandlers/aws/resourcelog/ec2_instance.go create mode 100644 pkg/resourcehandlers/aws/resourcelog/registry.go create mode 100644 pkg/resourcehandlers/aws/types/credential.go create mode 100644 pkg/resourcehandlers/azure/resourcehandler.go create mode 100644 pkg/resourcehandlers/azure/types/credential.go create mode 100644 pkg/resourcehandlers/docker/resourcehandler.go create mode 100644 pkg/resourcehandlers/google/resourcehandler.go create mode 100644 pkg/resourcehandlers/google/types/credential.go create mode 100644 pkg/resourcehandlers/k8s/driver.go create mode 100644 pkg/resourcehandlers/k8s/helper.go create mode 100644 pkg/resourcehandlers/k8s/intercept/converter_tf.go create mode 100644 pkg/resourcehandlers/k8s/intercept/enforcer_accessible.go create mode 100644 pkg/resourcehandlers/k8s/intercept/enforcer_composite.go create mode 100644 pkg/resourcehandlers/k8s/intercept/enforcer_operable.go create mode 100644 pkg/resourcehandlers/k8s/intercept/tf_types.go create mode 100644 pkg/resourcehandlers/k8s/intercept/types.go create mode 100644 pkg/resourcehandlers/k8s/key/codec.go create mode 100644 pkg/resourcehandlers/k8s/kube/namspaced_name.go create mode 100644 pkg/resourcehandlers/k8s/kube/pod.go create mode 100644 pkg/resourcehandlers/k8s/kubelabel/labels.go create mode 100644 pkg/resourcehandlers/k8s/polymorphic/selectors.go create mode 100644 pkg/resourcehandlers/k8s/polymorphic/serializer.go create mode 100644 pkg/resourcehandlers/k8s/resourcehandler.go create mode 100644 pkg/resourcehandlers/registry.go create mode 100644 pkg/resourcehandlers/types/credentials.go create mode 100644 pkg/resourcehandlers/types/util.go create mode 100644 pkg/resourcehandlers/types/wrap_resource.go create mode 100644 pkg/resourcehandlers/unknown/resourcehandler.go create mode 100644 pkg/resourcehandlers/validate.go create mode 100644 pkg/systemkuberes/connector.go create mode 100644 pkg/webhooks/walruscore/connector_binding.go diff --git a/go.mod b/go.mod index 025c772d3..2ebb0a70b 100644 --- a/go.mod +++ b/go.mod @@ -38,12 +38,21 @@ replace ( ) require ( + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 github.com/Masterminds/sprig/v3 v3.2.3 + github.com/aliyun/alibaba-cloud-sdk-go v1.62.717 + github.com/aliyun/aliyun_assist_client v0.0.0-20240201065944-028f1de47ef4 github.com/argoproj/argo-cd/v2 v2.0.0-00010101000000-000000000000 github.com/argoproj/argo-workflows/v3 v3.0.0-00010101000000-000000000000 github.com/argoproj/gitops-engine v0.7.1-0.20240411122334-1ade3a199867 + github.com/aws/aws-sdk-go-v2 v1.26.1 + github.com/aws/aws-sdk-go-v2/config v1.25.12 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.157.0 + github.com/aws/aws-sdk-go-v2/service/ssm v1.49.5 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dexidp/dex v0.0.0-20240322201621-f6114706f62e + github.com/docker/docker v24.0.7+incompatible github.com/drone/go-scm v0.0.0-00010101000000-000000000000 github.com/evanphx/json-patch v5.9.0+incompatible github.com/getkin/kin-openapi v0.122.0 @@ -59,6 +68,7 @@ require ( github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72 github.com/hashicorp/terraform-json v0.21.0 github.com/lestrrat-go/jwx v1.2.29 + github.com/mmmorris1975/ssm-session-client v0.400.1 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/prometheus/client_golang v1.19.0 github.com/robfig/cron/v3 v3.0.1 @@ -72,11 +82,13 @@ require ( github.com/tidwall/gjson v1.17.1 github.com/tidwall/sjson v1.2.5 github.com/zclconf/go-cty v1.14.1 - go.uber.org/atomic v1.10.0 + go.uber.org/atomic v1.11.0 go.uber.org/automaxprocs v1.5.3 + go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 golang.org/x/mod v0.15.0 + google.golang.org/api v0.171.0 helm.sh/helm/v3 v3.14.3 k8s.io/api v0.29.2 k8s.io/apiextensions-apiserver v0.29.2 @@ -103,8 +115,11 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/AppsFlyer/go-sundheit v0.5.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -122,6 +137,17 @@ require ( github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/argoproj/pkg v0.13.7-0.20230901113346-235a5432ec98 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect + github.com/aws/smithy-go v1.20.2 // indirect github.com/beevik/etree v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -132,18 +158,19 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/console v1.0.3 // indirect github.com/containerd/containerd v1.7.12 // indirect github.com/containerd/log v0.1.0 // indirect github.com/coreos/go-oidc/v3 v3.10.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/creack/goselect v0.1.2 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dexidp/dex/api/v2 v2.1.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/docker/cli v24.0.7+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v24.0.7+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 @@ -173,6 +200,7 @@ require ( github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/cel-go v0.17.7 // indirect @@ -201,6 +229,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.2.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -208,6 +237,8 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.0 // indirect + github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect @@ -215,6 +246,7 @@ require ( github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lestrrat-go/strftime v1.0.4 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -236,10 +268,12 @@ require ( github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + 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.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.0 // indirect @@ -279,7 +313,6 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/net v0.22.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect @@ -290,7 +323,6 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.18.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/api v0.171.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect @@ -298,6 +330,7 @@ require ( google.golang.org/grpc v1.62.1 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 4cf2e808a..9fe7019ea 100644 --- a/go.sum +++ b/go.sum @@ -608,6 +608,19 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AppsFlyer/go-sundheit v0.5.0 h1:/VxpyigCfJrq1r97mn9HPiAB2qrhcTFHwNIIDr15CZM= github.com/AppsFlyer/go-sundheit v0.5.0/go.mod h1:2ZM0BnfqT/mljBQO224VbL5XH06TgWuQ6Cn+cTtCpTY= +github.com/Azure/azure-sdk-for-go v66.0.0+incompatible h1:bmmC38SlE8/E81nNADlgmVGurPWMHDX2YNXVQMrBpEE= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 h1:d81/ng9rET2YqdVkVwkb6EXeRrLJIwyGnJcAlAWKwhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= 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/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= @@ -619,12 +632,15 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +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.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/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.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= @@ -653,6 +669,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agiledragon/gomonkey/v2 v2.11.0 h1:5oxSgA+tC1xuGsrIorR+sYiziYltmJyEZ9qA25b6l5U= +github.com/agiledragon/gomonkey/v2 v2.11.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -671,6 +689,10 @@ github.com/alicebob/miniredis/v2 v2.30.4 h1:8S4/o1/KoUArAGbGwPxcwf0krlzceva2XVOS github.com/alicebob/miniredis/v2 v2.30.4/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg= github.com/alitto/pond v1.8.3 h1:ydIqygCLVPqIX/USe5EaV/aSRXTRXDEI9JwuDdu+/xs= github.com/alitto/pond v1.8.3/go.mod h1:CmvIIGd5jKLasGI3D87qDkQxjzChdKMmnXMg3fG6M6Q= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.717 h1:bITTCQB3a+FpqwMplETi7CoI6ZgNpPfuFtfAE/GiRY4= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.717/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/aliyun/aliyun_assist_client v0.0.0-20240201065944-028f1de47ef4 h1:c0QgObue2BOSU92TdQd/zzW5JE2lTc1g6ksazFPyZCU= +github.com/aliyun/aliyun_assist_client v0.0.0-20240201065944-028f1de47ef4/go.mod h1:NNmtwNWmM/RmOPO9u6MJDh2/3cMyppmm+VaEsc/pPg4= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= @@ -690,7 +712,54 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.44.76/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.45.1/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v1.17.1/go.mod h1:JLnGeGONAyi2lWXI1p0PCIOIy333JMVK1U7Hf0aRFLw= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/config v1.17.10/go.mod h1:/4np+UiJJKpWHN7Q+LZvqXYgyjgeXm5+lLfDI6TPZao= +github.com/aws/aws-sdk-go-v2/config v1.25.12 h1:mF4cMuNh/2G+d19nWnm1vJ/ak0qK6SbqF0KtSX9pxu0= +github.com/aws/aws-sdk-go-v2/config v1.25.12/go.mod h1:lOvvqtZP9p29GIjOTuA/76HiVk0c/s8qRcFRq2+E2uc= +github.com/aws/aws-sdk-go-v2/credentials v1.12.23/go.mod h1:0awX9iRr/+UO7OwRQFpV1hNtXxOVuehpjVEzrIAYNcA= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19/go.mod h1:6Q0546uHDp421okhmmGfbxzq2hBqbXFNpi4k+Q1JnQA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26/go.mod h1:Y2OJ+P+MC1u1VKnavT+PshiEuGPyh/7DqxoDNij4/bg= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 h1:uR9lXYjdPX0xY+NhvaJ4dD8rpSRz5VY81ccIIoNG+lw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.64.0/go.mod h1:zul71QqzR4D1a90/5FloZiAnZ1CtuIjVH7R9MP997+A= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.157.0 h1:BCNvChkZM4xqssztw+rFllaDnoS4Hm6bZ20XBj8RsI0= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.157.0/go.mod h1:xejKuuRDjz6z5OqyeLsz01MlOqqW7CqpAB4PabNvpu8= +github.com/aws/aws-sdk-go-v2/service/ec2instanceconnect v1.14.11/go.mod h1:E29Z9YWBhILsNzaxWab92P6Wni6pdd4NVN8D4FCyNUU= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19/go.mod h1:02CP6iuYP+IVnBX5HULVdSAku/85eHB2Y9EsFhrkEwU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= +github.com/aws/aws-sdk-go-v2/service/ssm v1.31.3/go.mod h1:rEsqsZrOp9YvSGPOrcL3pR9+i/QJaWRkAYbuxMa7yCU= +github.com/aws/aws-sdk-go-v2/service/ssm v1.49.5 h1:KBwyHzP2QG8J//hoGuPyHWZ5tgL1BzaoMURUkecpI4g= +github.com/aws/aws-sdk-go-v2/service/ssm v1.49.5/go.mod h1:Ebk/HZmGhxWKDVxM4+pwbxGjm3RQOQLMjAEosI3ss9Q= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= +github.com/aws/aws-sdk-go-v2/service/sts v1.17.1/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= +github.com/aws/session-manager-plugin v0.0.0-20221012155945-c523002ee02c/go.mod h1:7n17tunRPUsniNBu5Ja9C7WwJWTdOzaLqr/H0Ns3uuI= +github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU= github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= @@ -734,6 +803,7 @@ github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHe 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/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= @@ -754,6 +824,8 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= @@ -772,6 +844,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4= github.com/creack/pty v1.1.20/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -789,6 +863,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg= github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= @@ -811,6 +887,7 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -859,6 +936,7 @@ github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0X github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= @@ -940,8 +1018,11 @@ github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= 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/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -1151,7 +1232,9 @@ github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= @@ -1186,6 +1269,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54 h1:0SMHxjkLKNawqUjjnMlCtEdj6uWZjv0+qDZ3F6GOADI= +github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54/go.mod h1:bm7MVZZvHQBfqHG5X59jrRE/3ak6HvK+/Zb6aZhLR2s= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -1207,6 +1292,8 @@ github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= @@ -1216,6 +1303,8 @@ github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtX github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8= +github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -1267,6 +1356,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh 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/mmmorris1975/ssm-session-client v0.400.1 h1:WrKlegOjn4geYte3i7wYn3flONGW0JmshMThjMYAKno= +github.com/mmmorris1975/ssm-session-client v0.400.1/go.mod h1:Gv045ehxc1lwPCiaaYNrjdRSBctFaIfTOSb3y8MpQ8E= 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= @@ -1293,6 +1384,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -1343,6 +1435,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 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/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -1358,6 +1452,8 @@ github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -1478,6 +1574,11 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/twinj/uuid v0.0.0-20151029044442-89173bcdda19/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= @@ -1503,6 +1604,7 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xtaci/smux v1.5.16/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= 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= @@ -1570,8 +1672,9 @@ go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lI go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -1594,6 +1697,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= @@ -1726,6 +1830,7 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= @@ -1858,6 +1963,7 @@ golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/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-20210616045830-e2b7044e8c71/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-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2338,12 +2444,14 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= diff --git a/pkg/apistatus/connector_status.go b/pkg/apistatus/connector_status.go new file mode 100644 index 000000000..e884776b3 --- /dev/null +++ b/pkg/apistatus/connector_status.go @@ -0,0 +1,32 @@ +package apistatus + +import v1 "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + +const ( + ConnectorConditionConnected v1.ConditionType = "Connected" +) + +const ( + ConnectorConditionReasonConnecting = "Connecting" + ConnectorConditionReasonDisconnected = "Disconnected" +) + +// connectorStatusPaths makes the following decision. +// +// | Condition Type | Condition Status | Human Readable Status | Human Sensible Status | +// | ---------------- | ----------------------- | --------------------- | --------------------- |\ +// | Connected | Unknown | Connecting | Transitioning | +// | Connected | False | Disconnected | Error | +// | Connected | True | Connected | | +var connectorStatusPaths = NewWalker( + [][]v1.ConditionType{ + { + ConnectorConditionConnected, + }, + }, +) + +// WalkConnector walks the given status by connector flow. +func WalkConnector(st *v1.StatusDescriptor) *v1.ConditionSummary { + return connectorStatusPaths.Walk(st) +} diff --git a/pkg/controllers/setup.go b/pkg/controllers/setup.go index e94102a60..377ee374b 100644 --- a/pkg/controllers/setup.go +++ b/pkg/controllers/setup.go @@ -18,6 +18,7 @@ var setupers = []controller.Setup{ new(walrus.SubjectAuthzReconciler), new(walruscore.CatalogReconciler), new(walruscore.ConnectorReconciler), + new(walruscore.ConnectorBindingReconciler), new(walruscore.ResourceReconciler), new(walruscore.ResourceDefinitionReconciler), new(walruscore.TemplateReconciler), diff --git a/pkg/controllers/walrus/project_subject.authz.go b/pkg/controllers/walrus/project_subject.authz.go index 80ea6c30c..96bcce03d 100644 --- a/pkg/controllers/walrus/project_subject.authz.go +++ b/pkg/controllers/walrus/project_subject.authz.go @@ -20,6 +20,7 @@ import ( ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile" walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" "github.com/seal-io/walrus/pkg/controller" "github.com/seal-io/walrus/pkg/kubeclientset" "github.com/seal-io/walrus/pkg/kubemeta" @@ -159,7 +160,7 @@ func (r *ProjectSubjectAuthzReconciler) Reconcile(ctx context.Context, req ctrl. RoleRef: rb.RoleRef, Subjects: rb.Subjects, } - if env.Spec.Type == walrus.EnvironmentTypeProduction && subj.Spec.Role == walrus.SubjectRoleUser { + if env.Spec.Type == walruscore.EnvironmentTypeProduction && subj.Spec.Role == walrus.SubjectRoleUser { eRb.RoleRef.Name = systemauthz.ConvertClusterRoleNameFromProjectRole(walrus.ProjectRoleViewer) } systemmeta.NoteResource(eRb, "rolebindings", map[string]string{ diff --git a/pkg/controllers/walruscore/connector.go b/pkg/controllers/walruscore/connector.go index 3f163d743..981347508 100644 --- a/pkg/controllers/walruscore/connector.go +++ b/pkg/controllers/walruscore/connector.go @@ -3,29 +3,134 @@ package walruscore import ( "context" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" + ctrlbuilder "sigs.k8s.io/controller-runtime/pkg/builder" + ctrlcli "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + ctrlpredicate "sigs.k8s.io/controller-runtime/pkg/predicate" ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile" walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + "github.com/seal-io/walrus/pkg/apistatus" "github.com/seal-io/walrus/pkg/controller" + "github.com/seal-io/walrus/pkg/kubeclientset" + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/resourcehandlers" + "github.com/seal-io/walrus/pkg/systemmeta" ) // ConnectorReconciler reconciles a v1.Connector object. -type ConnectorReconciler struct{} +type ConnectorReconciler struct { + client ctrlcli.Client +} var _ ctrlreconcile.Reconciler = (*ConnectorReconciler)(nil) func (r *ConnectorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = ctrllog.FromContext(ctx) + logger := ctrllog.FromContext(ctx) + + obj := new(walruscore.Connector) + err := r.client.Get(ctx, req.NamespacedName, obj) + if err != nil { + return ctrl.Result{}, ctrlcli.IgnoreNotFound(err) + } + + if obj.DeletionTimestamp != nil { + // Return if already unlocked. + if systemmeta.Unlock(obj) { + return ctrl.Result{}, nil + } + + sec := &core.Secret{ + ObjectMeta: meta.ObjectMeta{ + Namespace: obj.Namespace, + Name: obj.Spec.SecretName, + }, + } + + err = r.client.Get(ctx, ctrlcli.ObjectKeyFromObject(sec), sec) + if err != nil { + return ctrl.Result{}, err + } + + if sec.DeletionTimestamp == nil { + err = r.client.Delete(ctx, sec) + if err != nil { + return ctrl.Result{}, err + } + } + + // Unlock. + _, err = kubeclientset.UpdateWithCtrlClient(ctx, r.client, obj) + if err != nil { + logger.Error(err, "failed to unlock connector", "connector", obj) + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil + } - // TODO: your logic here + if apistatus.ConnectorConditionConnected.IsTrue(obj) || apistatus.ConnectorConditionConnected.IsFalse(obj) { + return ctrl.Result{}, nil + } + // Lock if not. + if !systemmeta.Lock(obj) { + obj, err = kubeclientset.UpdateWithCtrlClient(ctx, r.client, obj) + if err != nil { + logger.Error(err, "failed to lock connector", "connector", obj) + return ctrl.Result{}, err + } + } + + rh, err := resourcehandlers.Get(ctx, resourcehandler.CreateOptions{ + Connector: *obj, + }) + if err != nil { + logger.Error(err, "fetch resource handler") + return ctrl.Result{}, ctrlcli.IgnoreNotFound(err) + } + + err = rh.IsConnected(ctx) + if err != nil { + apistatus.ConnectorConditionConnected.False(obj, apistatus.ConnectorConditionReasonDisconnected, err.Error()) + } else { + apistatus.ConnectorConditionConnected.True(obj, "", "") + obj.Status.Project = systemmeta.GetProjectName(obj.Namespace) + } + + // Update status. + { + err = r.updateStatus(ctx, obj) + if err != nil { + return ctrl.Result{}, err + } + } return ctrl.Result{}, nil } +// updateStatus updates the status of the given connector. +func (r *ConnectorReconciler) updateStatus(ctx context.Context, obj *walruscore.Connector) error { + existed := new(walruscore.Connector) + err := r.client.Get(ctx, ctrlcli.ObjectKeyFromObject(obj), existed) + if err != nil { + return err + } + + existed.Status = obj.Status + existed.Status.ConditionSummary = *apistatus.WalkConnector(&existed.Status.StatusDescriptor) + + return r.client.Status().Update(ctx, existed) +} + func (r *ConnectorReconciler) SetupController(_ context.Context, opts controller.SetupOptions) error { + r.client = opts.Manager.GetClient() + return ctrl.NewControllerManagedBy(opts.Manager). - For(&walruscore.Connector{}). + For(&walruscore.Connector{}, + ctrlbuilder.WithPredicates(ctrlpredicate.GenerationChangedPredicate{}), + ). Complete(r) } diff --git a/pkg/controllers/walruscore/connector_binding.go b/pkg/controllers/walruscore/connector_binding.go new file mode 100644 index 000000000..3caf93e8f --- /dev/null +++ b/pkg/controllers/walruscore/connector_binding.go @@ -0,0 +1,160 @@ +package walruscore + +import ( + "context" + "fmt" + "strings" + "time" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + ctrlbuilder "sigs.k8s.io/controller-runtime/pkg/builder" + ctrlcli "sigs.k8s.io/controller-runtime/pkg/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + ctrlpredicate "sigs.k8s.io/controller-runtime/pkg/predicate" + ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + "github.com/seal-io/walrus/pkg/controller" + "github.com/seal-io/walrus/pkg/kubeclientset" + "github.com/seal-io/walrus/pkg/systemmeta" +) + +// ConnectorBindingReconciler reconciles a v1.ConnectorBinding object. +type ConnectorBindingReconciler struct { + client ctrlcli.Client +} + +var _ ctrlreconcile.Reconciler = (*ConnectorBindingReconciler)(nil) + +func (r *ConnectorBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := ctrllog.FromContext(ctx) + + cb := new(walruscore.ConnectorBinding) + err := r.client.Get(ctx, req.NamespacedName, cb) + if err != nil { + logger.Error(err, "failed to get ConnectorBinding", "namespace", req.Namespace, "name", req.Name) + return ctrl.Result{}, ctrlcli.IgnoreNotFound(err) + } + + if cb.DeletionTimestamp != nil { + if systemmeta.Unlock(cb) { + return ctrl.Result{}, nil + } + + // Unlabel environment. + envList := &walrus.EnvironmentList{} + if err := r.client.List(ctx, envList, ctrlcli.MatchingFields{"metadata.name": cb.Namespace}); err != nil { + logger.Error(err, "failed to list Environments for ConnectorBinding", "namespace", cb.Namespace) + return ctrl.Result{}, err + } + + if len(envList.Items) == 1 { + env := envList.Items[0] + + labels := env.GetLabels() + delete(labels, walruscore.ProviderLabelPrefix+strings.ToLower(cb.Status.Type)) + env.SetLabels(labels) + + err = r.client.Update(ctx, &env) + if err != nil { + logger.Error(err, "failed to update Environment", "name", env.Name) + return ctrl.Result{}, ctrlcli.IgnoreNotFound(err) + } + } + + // Unlock. + _, err = kubeclientset.UpdateWithCtrlClient(ctx, r.client, cb) + if err != nil { + logger.Error(err, "failed to unlock ConnectorBinding", "namespace", cb.Namespace, "name", cb.Name) + return ctrl.Result{}, ctrlcli.IgnoreNotFound(err) + } + + return ctrl.Result{}, nil + } + + // Lock if not. + if !systemmeta.Lock(cb) { + cb, err = kubeclientset.UpdateWithCtrlClient(ctx, r.client, cb) + if err != nil { + logger.Error(err, "failed to lock ConnectorBinding", "namespace", cb.Namespace, "name", cb.Name) + return ctrl.Result{}, err + } + } + + conn := &walruscore.Connector{ + ObjectMeta: meta.ObjectMeta{ + Name: cb.Spec.Connector.Name, + Namespace: cb.Spec.Connector.Namespace, + }, + } + + err = r.client.Get(ctx, ctrlcli.ObjectKeyFromObject(conn), conn) + if err != nil { + return ctrl.Result{}, err + } + + if cb.Status.Type != conn.Spec.Type || cb.Status.Category != conn.Spec.Category { + cb.Status.Type = conn.Spec.Type + cb.Status.Category = conn.Spec.Category + + err = r.client.Status().Update(ctx, cb) + if err != nil { + return ctrl.Result{}, err + } + } + + // Label environment. + envList := &walrus.EnvironmentList{} + if err := r.client.List(ctx, envList, ctrlcli.MatchingFields{"metadata.name": cb.Namespace}); err != nil { + logger.Error(err, "failed to list Environments for ConnectorBinding", "namespace", cb.Namespace) + return ctrl.Result{}, err + } + + if len(envList.Items) != 1 { + // NB: we should never reach here. + logger.Error(nil, "cannot fetch corresponding environment") + return ctrl.Result{RequeueAfter: time.Second}, nil + } + + env := envList.Items[0] + + labels := env.Labels + if labels == nil { + labels = make(map[string]string) + } + + labels[walruscore.ProviderLabelPrefix+strings.ToLower(cb.Status.Type)] = "true" + env.SetLabels(labels) + + err = r.client.Update(ctx, &env) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *ConnectorBindingReconciler) SetupController(ctx context.Context, opts controller.SetupOptions) error { + r.client = opts.Manager.GetClient() + + // Configure field indexer. + fi := opts.Manager.GetFieldIndexer() + err := fi.IndexField(ctx, &walrus.Environment{}, "metadata.name", + func(obj ctrlcli.Object) []string { + if obj == nil { + return nil + } + return []string{obj.GetName()} + }) + if err != nil { + return fmt.Errorf("index environment 'metadata.name': %w", err) + } + + return ctrl.NewControllerManagedBy(opts.Manager). + For(&walruscore.ConnectorBinding{}, + ctrlbuilder.WithPredicates(ctrlpredicate.GenerationChangedPredicate{}), + ). + Complete(r) +} diff --git a/pkg/extensionapis/setup.go b/pkg/extensionapis/setup.go index 50adf70c8..9b32c5ec4 100644 --- a/pkg/extensionapis/setup.go +++ b/pkg/extensionapis/setup.go @@ -21,6 +21,7 @@ import ( var setupers = []extensionapi.Setup{ new(walrus.CatalogHandler), new(walrus.ConnectorHandler), + new(walrus.ConnectorBindingHandler), new(walrus.EnvironmentHandler), new(walrus.FileExampleHandler), new(walrus.ProjectHandler), diff --git a/pkg/extensionapis/walrus/connector.go b/pkg/extensionapis/walrus/connector.go index c08508a7a..51b992cb3 100644 --- a/pkg/extensionapis/walrus/connector.go +++ b/pkg/extensionapis/walrus/connector.go @@ -3,6 +3,7 @@ package walrus import ( "context" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/registry/rest" @@ -25,16 +26,66 @@ func (h *ConnectorHandler) SetupHandler( ctx context.Context, opts extensionapi.SetupOptions, ) (gvr schema.GroupVersionResource, srs map[string]rest.Storage, err error) { + // Configure field indexer. + fi := opts.Manager.GetFieldIndexer() + err = fi.IndexField(ctx, &walruscore.Connector{}, "metadata.name", + func(obj ctrlcli.Object) []string { + if obj == nil { + return nil + } + return []string{obj.GetName()} + }) + if err != nil { + return schema.GroupVersionResource{}, nil, err + } + // Declare GVR. gvr = walrus.SchemeGroupVersionResource("connectors") + // Create table convertor to pretty the kubectl's output. + var tc rest.TableConvertor + { + tc, err = extensionapi.NewJSONPathTableConvertor( + extensionapi.JSONPathTableColumnDefinition{ + TableColumnDefinition: meta.TableColumnDefinition{ + Name: "Category", + Type: "string", + }, + JSONPath: ".spec.category", + }, + extensionapi.JSONPathTableColumnDefinition{ + TableColumnDefinition: meta.TableColumnDefinition{ + Name: "Type", + Type: "string", + }, + JSONPath: ".spec.type", + }, + extensionapi.JSONPathTableColumnDefinition{ + TableColumnDefinition: meta.TableColumnDefinition{ + Name: "Applicable Environment Type", + Type: "string", + }, + JSONPath: ".spec.applicableEnvironmentType", + }, + extensionapi.JSONPathTableColumnDefinition{ + TableColumnDefinition: meta.TableColumnDefinition{ + Name: "Project", + Type: "string", + }, + JSONPath: ".status.project", + }) + if err != nil { + return gvr, nil, err + } + } + // As storage. h.ObjectInfo = &walrus.Connector{} h.CurdOperations = extensionapi.WithCurdProxy[ *walrus.Connector, *walrus.ConnectorList, *walruscore.Connector, *walruscore.ConnectorList, - ](nil, h, opts.Manager.GetClient().(ctrlcli.WithWatch), opts.Manager.GetAPIReader()) + ](tc, h, opts.Manager.GetClient().(ctrlcli.WithWatch), opts.Manager.GetAPIReader()) - return + return gvr, nil, nil } var ( diff --git a/pkg/extensionapis/walrus/connector_binding.go b/pkg/extensionapis/walrus/connector_binding.go new file mode 100644 index 000000000..34a1d688c --- /dev/null +++ b/pkg/extensionapis/walrus/connector_binding.go @@ -0,0 +1,125 @@ +package walrus + +import ( + "context" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" + ctrlcli "sigs.k8s.io/controller-runtime/pkg/client" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + "github.com/seal-io/walrus/pkg/extensionapi" +) + +// ConnectorBindingHandler handles v1.ConnectorBinding objects. +// +// ConnectorBindingHandler proxies the v1.ConnectorBinding objects to the walrus core. +type ConnectorBindingHandler struct { + extensionapi.ObjectInfo + extensionapi.CurdOperations +} + +func (h *ConnectorBindingHandler) SetupHandler( + ctx context.Context, + opts extensionapi.SetupOptions, +) (gvr schema.GroupVersionResource, srs map[string]rest.Storage, err error) { + // Configure field indexer. + fi := opts.Manager.GetFieldIndexer() + err = fi.IndexField(ctx, &walruscore.ConnectorBinding{}, "metadata.name", + func(obj ctrlcli.Object) []string { + if obj == nil { + return nil + } + return []string{obj.GetName()} + }) + if err != nil { + return schema.GroupVersionResource{}, nil, err + } + + // Declare GVR. + gvr = walrus.SchemeGroupVersionResource("connectorbindings") + + // Create table convertor to pretty the kubectl's output. + var tc rest.TableConvertor + { + tc, err = extensionapi.NewJSONPathTableConvertor( + extensionapi.JSONPathTableColumnDefinition{ + TableColumnDefinition: meta.TableColumnDefinition{ + Name: "Connector", + Type: "string", + }, + JSONPath: ".spec.connector.name", + }, + extensionapi.JSONPathTableColumnDefinition{ + TableColumnDefinition: meta.TableColumnDefinition{ + Name: "Connector Namespace", + Type: "string", + }, + JSONPath: ".spec.connector.namespace", + }, + extensionapi.JSONPathTableColumnDefinition{ + TableColumnDefinition: meta.TableColumnDefinition{ + Name: "Connector Type", + Type: "string", + }, + JSONPath: ".status.Type", + }, + extensionapi.JSONPathTableColumnDefinition{ + TableColumnDefinition: meta.TableColumnDefinition{ + Name: "Connector Category", + Type: "string", + }, + JSONPath: ".status.Category", + }, + ) + if err != nil { + return gvr, nil, err + } + } + + // As storage. + h.ObjectInfo = &walrus.ConnectorBinding{} + h.CurdOperations = extensionapi.WithCurdProxy[ + *walrus.ConnectorBinding, *walrus.ConnectorBindingList, *walruscore.ConnectorBinding, *walruscore.ConnectorBindingList, + ](tc, h, opts.Manager.GetClient().(ctrlcli.WithWatch), opts.Manager.GetAPIReader()) + + return gvr, nil, nil +} + +var ( + _ rest.Storage = (*ConnectorBindingHandler)(nil) + _ rest.Creater = (*ConnectorBindingHandler)(nil) + _ rest.Lister = (*ConnectorBindingHandler)(nil) + _ rest.Getter = (*ConnectorBindingHandler)(nil) + _ rest.Updater = (*ConnectorBindingHandler)(nil) + _ rest.Patcher = (*ConnectorBindingHandler)(nil) + _ rest.Watcher = (*ConnectorBindingHandler)(nil) + _ rest.CollectionDeleter = (*ConnectorBindingHandler)(nil) + _ rest.GracefulDeleter = (*ConnectorBindingHandler)(nil) +) + +func (h *ConnectorBindingHandler) New() runtime.Object { + return &walrus.ConnectorBinding{} +} + +func (h *ConnectorBindingHandler) Destroy() { +} + +func (h *ConnectorBindingHandler) NewList() runtime.Object { + return &walrus.ConnectorBindingList{} +} + +func (h *ConnectorBindingHandler) NewListForProxy() runtime.Object { + return &walruscore.ConnectorBindingList{} +} + +func (h *ConnectorBindingHandler) CastObjectTo(do *walrus.ConnectorBinding) *walruscore.ConnectorBinding { + return (*walruscore.ConnectorBinding)(do) +} + +func (h *ConnectorBindingHandler) CastObjectFrom(uo *walruscore.ConnectorBinding) *walrus.ConnectorBinding { + return (*walrus.ConnectorBinding)(uo) +} diff --git a/pkg/extensionapis/walrus/environment.go b/pkg/extensionapis/walrus/environment.go index f4f35c72a..6de547828 100644 --- a/pkg/extensionapis/walrus/environment.go +++ b/pkg/extensionapis/walrus/environment.go @@ -23,6 +23,7 @@ import ( ctrlcli "sigs.k8s.io/controller-runtime/pkg/client" walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" "github.com/seal-io/walrus/pkg/extensionapi" "github.com/seal-io/walrus/pkg/kubemeta" "github.com/seal-io/walrus/pkg/systemkuberes" @@ -410,7 +411,7 @@ func convertEnvironmentFromNamespace(ns *core.Namespace) *walrus.Environment { env := &walrus.Environment{ ObjectMeta: ns.ObjectMeta, Spec: walrus.EnvironmentSpec{ - Type: walrus.EnvironmentType(notes["type"]), + Type: walruscore.EnvironmentType(notes["type"]), DisplayName: notes["displayName"], Description: notes["description"], }, diff --git a/pkg/resourcehandler/consts.go b/pkg/resourcehandler/consts.go new file mode 100644 index 000000000..e3cf57f92 --- /dev/null +++ b/pkg/resourcehandler/consts.go @@ -0,0 +1,10 @@ +package resourcehandler + +const ( + ConnectorTypeDocker string = "Docker" + ConnectorTypeKubernetes string = "Kubernetes" + ConnectorTypeAlibabaCloud string = "AlibabaCloud" + ConnectorTypeAWS string = "AWS" + ConnectorTypeAzure string = "Azure" + ConnectorTypeGoogle string = "Google" +) diff --git a/pkg/resourcehandler/types.go b/pkg/resourcehandler/types.go new file mode 100644 index 000000000..7749b8928 --- /dev/null +++ b/pkg/resourcehandler/types.go @@ -0,0 +1,107 @@ +package resourcehandler + +import ( + "context" + "io" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" +) + +// Type indicates the type of ResourceHandler, +// e.g. Kubernetes, AWS, etc. +type Type = string + +// ResourceHandler holds the actions that an resourcehandlers must satisfy. +type ResourceHandler interface { + // Type returns Type. + Type() Type + + // IsConnected validates whether is connected. + IsConnected(context.Context) error + + // Burst returns the maximum number of operations that can be called at once. + Burst() int + + // ID returns an operation identifier of the resourcehandlers. + // + // The result is not a unique notation of in the traditional sense, + // which means we can't use it to identify the only resourcehandlers, + // but rather a generalization of the characteristics of the resourcehandlers. + // + // If two operators have the same ID, + // we can group them together for some operations. + // + // ID returns a blank string if no that kind of identifier. + ID() string + + // GetKeys returns keys from the given resource. + // + // The given walrus.ResourceComponent item must specify the following fields: + // ID, DeployerType, Type and Name. + GetKeys(context.Context, *walrus.ResourceComponents) (*ResourceComponentOperationKeys, error) + + // GetStatus gets status of the given resource. + // + // The given walrus.ResourceComponent item must specify the following fields: + // ID, DeployerType, Type and Name. + GetStatus(context.Context, *walrus.ResourceComponents) ([]meta.Condition, error) + + // GetComponents gets components of the given resource, + // returns list must not be `nil` unless unexpected input or raising error, + // it can be used to clean stale items safety if got an empty list. + GetComponents(context.Context, *walrus.ResourceComponents) ([]*walrus.ResourceComponents, error) + + // Log gets logs from the given key. + Log(context.Context, string, LogOptions) error + + // Exec executes commands to the given key. + Exec(context.Context, string, ExecOptions) error +} + +// LogOptions holds the options of ResourceHandler's Log action. +type LogOptions struct { + // Out receives the output. + Out io.Writer + // WithoutFollow returns logs without following. + WithoutFollow bool + // Previous indicates to get the previous log of the accessing target. + Previous bool + // SinceSeconds returns logs newer than a relative duration. + SinceSeconds *int64 + // Timestamps returns logs with RFC3339 or RFC3339Nano timestamp. + Timestamps bool + // TailLines indicates to get the lines from end of the logs. + TailLines *int64 +} + +// ExecOptions holds the options of ResourceHandler's Exec action. +type ExecOptions struct { + // Out receives the output. + Out io.Writer + // In passes the input. + In io.Reader + // Shell indicates to launch what kind of shell. + Shell string + // Resizer indicates to resize the size(width, height) of the terminal. + Resizer TerminalResizer +} + +// TerminalResizer holds the options to resize the terminal. +type TerminalResizer interface { + // Next returns the new terminal size after the terminal has been resized. + // It returns false when monitoring has been stopped. + Next() (width, height uint16, ok bool) +} + +type CreateOptions struct { + Connector walruscore.Connector +} + +type Creator func(context.Context, CreateOptions) (ResourceHandler, error) + +type ResourceComponentOperationKeys struct { + // TODO: Need to consider how the resource component is used +} diff --git a/pkg/resourcehandlers/alibaba/key/key.go b/pkg/resourcehandlers/alibaba/key/key.go new file mode 100644 index 000000000..1cd1ca428 --- /dev/null +++ b/pkg/resourcehandlers/alibaba/key/key.go @@ -0,0 +1,26 @@ +package key + +import ( + "strings" + + "github.com/seal-io/utils/stringx" +) + +// Decode parses the given string into {resource type, resource name}, returns false if not a valid key. +func Decode(s string) (resourceType, name string, ok bool) { + ss := strings.SplitN(s, "/", 2) + + ok = len(ss) == 2 + if !ok { + return + } + resourceType = ss[0] + name = ss[1] + + return +} + +// Encode constructs the given {resource type, resource name} into a valid key. +func Encode(resourceType, name string) string { + return stringx.Join("/", resourceType, name) +} diff --git a/pkg/resourcehandlers/alibaba/resourceexec/common.go b/pkg/resourcehandlers/alibaba/resourceexec/common.go new file mode 100644 index 000000000..834df3dfc --- /dev/null +++ b/pkg/resourcehandlers/alibaba/resourceexec/common.go @@ -0,0 +1,3 @@ +package resourceexec + +const schemeHttps = "HTTPS" diff --git a/pkg/resourcehandlers/alibaba/resourceexec/ecs_instance.go b/pkg/resourcehandlers/alibaba/resourceexec/ecs_instance.go new file mode 100644 index 000000000..f9a46fb60 --- /dev/null +++ b/pkg/resourcehandlers/alibaba/resourceexec/ecs_instance.go @@ -0,0 +1,280 @@ +package resourceexec + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "time" + + "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" + assistlog "github.com/aliyun/aliyun_assist_client/agent/log" + assistclient "github.com/aliyun/aliyun_assist_client/agent/session/plugin" + "github.com/aliyun/aliyun_assist_client/agent/session/plugin/message" + "github.com/aliyun/aliyun_assist_client/thirdparty/sirupsen/logrus" + "github.com/seal-io/utils/pools/bytespool" + "github.com/seal-io/utils/pools/gopool" + "k8s.io/klog/v2" + + "github.com/seal-io/walrus/pkg/resourcehandler" + optypes "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +const ( + defaultTerminalHeight uint16 = 100 + defaultTerminalWidth uint16 = 100 +) + +const ( + // These configs are borrowed from aliyun repo + // nolint: lll + // https://github.com/aliyun/aliyun_assist_client/blob/d2b430f3fa8aea1abb376d0e4a08d9888729f1c4/agent/session/plugin/client.go#L34 + // maxPackageSend in Byte. + maxPackageSendSize = 2048 + // DefaultSendSpeed send speed in kbps, this is from aliyun repo. + defaultPackageSendSpeed = 200 + // DefaultSendInterval in ms, this is calculated from defaultSendSpeed and maxPackageSend. + defaultPackageSendInterval = 1000 / (defaultPackageSendSpeed * 1024 / 8 / maxPackageSendSize) + + defaultPackageSendIntervalDuration = defaultPackageSendInterval * time.Millisecond +) + +func init() { + logger := logrus.New() + logger.SetOutput(klog.NewKlogr()) + assistlog.Log = logger +} + +type ecsInstance struct { + cred *optypes.Credential + ecsCli *ecs.Client + assistCli *assistclient.Client + realConnected bool +} + +func getEcsInstance(ctx context.Context) (optypes.ExecutableResource, error) { + var ( + res ecsInstance + err error + ) + + res.cred, err = optypes.CredentialFromCtx(ctx) + if err != nil { + return nil, err + } + + res.ecsCli, err = ecs.NewClientWithAccessKey(res.cred.Region, res.cred.AccessKey, res.cred.AccessSecret) + if err != nil { + return nil, fmt.Errorf("error create alibaba ecs client %s: %w", res.cred.AccessKey, err) + } + + return &res, err +} + +// Supported check whether ecs instance support session manager. +func (r *ecsInstance) Supported(_ context.Context, name string) (bool, error) { + req := ecs.CreateDescribeCloudAssistantStatusRequest() + req.Scheme = schemeHttps + req.InstanceId = &[]string{name} + + resp, err := r.ecsCli.DescribeCloudAssistantStatus(req) + if err != nil { + return false, err + } + + icas := resp.InstanceCloudAssistantStatusSet.InstanceCloudAssistantStatus + if len(icas) == 0 || !icas[0].SupportSessionManager { + return false, nil + } + + return true, nil +} + +// Exec support data channel to input and output command. +func (r *ecsInstance) Exec(ctx context.Context, name string, opts resourcehandler.ExecOptions) error { + wsURL, err := r.getConnectAddress(name) + if err != nil { + return err + } + + readCloser := io.NopCloser(opts.In) + + r.assistCli, err = assistclient.NewClient( + wsURL, readCloser, opts.Out, false, "", true, true, + ) + if err != nil { + return err + } + + defer func() { + if !r.realConnected { + return + } + + if err := r.assistCli.SendCloseMessage(); err != nil { + klog.Warningf("error send close message: %v", err) + } + }() + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Connect to websocket URL. + if !r.assistCli.Connected { + if err = r.assistCli.Connect(); err != nil { + return fmt.Errorf("error connect: %w", err) + } + + r.assistCli.Conn.SetCloseHandler(func(int, string) (err error) { + cancel() + return + }) + } + + eg := gopool.GroupWithContextIn(ctx) + eg.Go(func(ctx context.Context) error { + return r.setTerminalSize(ctx, opts) + }) + + eg.Go(func(ctx context.Context) error { + return r.writeToConn(ctx) + }) + + eg.Go(func(ctx context.Context) error { + return r.readFromConn(ctx) + }) + + return eg.Wait() +} + +func (r *ecsInstance) setTerminalSize(ctx context.Context, opts resourcehandler.ExecOptions) error { + set := func(width, height uint16) error { + // Send resize data to remote connection. + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, int16(height)) + _ = binary.Write(buf, binary.LittleEndian, int16(width)) + + err := r.assistCli.SendResizeDataMessage(buf.Bytes()) + if err != nil { + return fmt.Errorf("error send resize data: %w", err) + } + + return nil + } + + // Without resizer. + if opts.Resizer == nil { + return set(defaultTerminalWidth, defaultTerminalHeight) + } + + // With resizer. + for { + select { + case <-ctx.Done(): + return nil + default: + } + + if !r.realConnected { + continue + } + + width, height, ok := opts.Resizer.Next() + if !ok { + return errors.New("invalid terminal resizer") + } + + err := set(width, height) + if err != nil { + return err + } + } +} + +func (r *ecsInstance) getConnectAddress(name string) (string, error) { + req := ecs.CreateStartTerminalSessionRequest() + req.InstanceId = &[]string{name} + + resp, err := r.ecsCli.StartTerminalSession(req) + if err != nil { + return "", fmt.Errorf("error start session for %s: %w", name, err) + } + + return resp.WebSocketUrl, nil +} + +func (r *ecsInstance) writeToConn(ctx context.Context) error { + buff := bytespool.GetBytes(maxPackageSendSize) + defer func() { bytespool.Put(buff) }() + + for { + // Watch done event. + select { + case <-ctx.Done(): + return nil + default: + } + + // Control send speed. + time.Sleep(defaultPackageSendIntervalDuration) + + // Read from opts in. + size, err := r.assistCli.Input.Read(buff) + if err != nil { + return fmt.Errorf("error read from user input: %w", err) + } + + // Write to connection. + if r.realConnected { + err = r.assistCli.SendStreamDataMessage(buff[:size]) + if err != nil { + return fmt.Errorf("error send data: %w", err) + } + } + } +} + +func (r *ecsInstance) readFromConn(ctx context.Context) error { + for { + // Watch done event. + select { + case <-ctx.Done(): + return nil + default: + } + + _, data, err := r.assistCli.Conn.ReadMessage() + if err != nil { + return fmt.Errorf("error read message: %w", err) + } + + msg := message.Message{} + + err = msg.Deserialize(data) + if err != nil { + return fmt.Errorf("error deserialize message: %w", err) + } + + err = msg.Validate() + if err != nil { + return fmt.Errorf("error validate message: %w", err) + } + + switch msg.MessageType { + case message.OutputStreamDataMessage: + r.realConnected = true + + _, err = r.assistCli.Output.Write(msg.Payload) + if err != nil { + return fmt.Errorf("error write message to output") + } + case message.StatusDataChannel: + err = r.assistCli.ProcessStatusDataChannel(msg.Payload) + if err != nil { + return fmt.Errorf("error process status message: %w", err) + } + } + } +} diff --git a/pkg/resourcehandlers/alibaba/resourceexec/registry.go b/pkg/resourcehandlers/alibaba/resourceexec/registry.go new file mode 100644 index 000000000..5e06f6e22 --- /dev/null +++ b/pkg/resourcehandlers/alibaba/resourceexec/registry.go @@ -0,0 +1,83 @@ +package resourceexec + +import ( + "context" + "errors" + + "k8s.io/klog/v2" + + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/resourcehandlers/alibaba/key" + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +// resourceTypes indicate supported resource type and their functions. +var resourceTypes map[string]getExecutableResource + +type getExecutableResource func(ctx context.Context) (types.ExecutableResource, error) + +func init() { + resourceTypes = map[string]getExecutableResource{ + "alicloud_instance": getEcsInstance, + } +} + +// Supported indicate whether the resource is supported to exec. +func Supported(ctx context.Context, k string) (bool, error) { + resourceType, name, ok := key.Decode(k) + if !ok { + return false, errors.New("invalid key") + } + + fs, exist := resourceTypes[resourceType] + if !exist { + return false, nil + } + + res, err := fs(ctx) + if err != nil { + return false, err + } + + supported, err := res.Supported(ctx, name) + if err != nil { + return false, err + } + + if !supported { + return false, nil + } + + return supported, nil +} + +// Exec resource by key. +func Exec(ctx context.Context, k string, opts resourcehandler.ExecOptions) error { + supported, err := Supported(ctx, k) + if err != nil { + return err + } + + if !supported { + return errors.New("unsupported resource type") + } + + resourceType, name, ok := key.Decode(k) + if !ok { + return errors.New("invalid key") + } + + fs := resourceTypes[resourceType] + + res, err := fs(ctx) + if err != nil { + return err + } + + err = res.Exec(ctx, name, opts) + if err != nil { + klog.Warningf("error exec resource %s/%s: %v", resourceType, name, err) + } + + return err +} diff --git a/pkg/resourcehandlers/alibaba/resourcehandler.go b/pkg/resourcehandlers/alibaba/resourcehandler.go new file mode 100644 index 000000000..c8c89706f --- /dev/null +++ b/pkg/resourcehandlers/alibaba/resourcehandler.go @@ -0,0 +1,112 @@ +package alibaba + +import ( + "context" + "fmt" + + "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" + "github.com/seal-io/utils/stringx" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/resourcehandlers/alibaba/resourceexec" + "github.com/seal-io/walrus/pkg/resourcehandlers/alibaba/resourcelog" + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +const OperatorType = resourcehandler.ConnectorTypeAlibabaCloud + +// New returns resourcehandlers.ResourceHandler with the given options. +func New(ctx context.Context, opts resourcehandler.CreateOptions) (resourcehandler.ResourceHandler, error) { + name := opts.Connector.Name + + config, err := types.GetConfigData(ctx, opts) + if err != nil { + return nil, err + } + + cred, err := types.GetCredential(config) + if err != nil { + return nil, err + } + + return Operator{ + name: name, + cred: cred, + identifier: stringx.SumBySHA256("alibaba:", cred.AccessKey, cred.AccessSecret), + }, nil +} + +type Operator struct { + name string + cred *types.Credential + identifier string +} + +func (op Operator) IsConnected(ctx context.Context) error { + client, err := ecs.NewClientWithAccessKey( + op.cred.Region, + op.cred.AccessKey, + op.cred.AccessSecret, + ) + if err != nil { + return fmt.Errorf("error create alibaba client %s: %w", op.name, err) + } + + // Use DescribeRegion API to check reachable and user has access to region. + // https://www.alibabacloud.com/help/en/elastic-compute-service/latest/regions-describeregions + req := ecs.CreateDescribeRegionsRequest() + req.Scheme = "HTTPS" + + _, err = client.DescribeRegions(req) + if err != nil { + return fmt.Errorf("error connect to %s: %w", op.name, err) + } + + return nil +} + +func (op Operator) Type() resourcehandler.Type { + return OperatorType +} + +// Burst implements resourcehandlers.ResourceHandler. +func (op Operator) Burst() int { + return 200 +} + +// ID implements resourcehandlers.ResourceHandler. +func (op Operator) ID() string { + return op.identifier +} + +// GetComponents implements resourcehandlers.ResourceHandler. +func (op Operator) GetComponents( + ctx context.Context, + resource *walrus.ResourceComponents, +) ([]*walrus.ResourceComponents, error) { + return nil, nil +} + +// Log implements resourcehandlers.ResourceHandler. +func (op Operator) Log(ctx context.Context, key string, opts resourcehandler.LogOptions) error { + newCtx := context.WithValue(ctx, types.CredentialKey, op.cred) + return resourcelog.Log(newCtx, key, opts) +} + +// Exec implements resourcehandlers.ResourceHandler. +func (op Operator) Exec(ctx context.Context, key string, opts resourcehandler.ExecOptions) error { + newCtx := context.WithValue(ctx, types.CredentialKey, op.cred) + return resourceexec.Exec(newCtx, key, opts) +} + +func (op Operator) GetKeys(ctx context.Context, component *walrus.ResourceComponents) (*resourcehandler.ResourceComponentOperationKeys, error) { + return nil, nil +} + +func (op Operator) GetStatus(ctx context.Context, component *walrus.ResourceComponents) ([]meta.Condition, error) { + // TODO: Implement this method after resource is migrated. + + return nil, nil +} diff --git a/pkg/resourcehandlers/alibaba/resourcelog/client.go b/pkg/resourcehandlers/alibaba/resourcelog/client.go new file mode 100644 index 000000000..6752f9c22 --- /dev/null +++ b/pkg/resourcehandlers/alibaba/resourcelog/client.go @@ -0,0 +1,26 @@ +package resourcelog + +import ( + "context" + "fmt" + + "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" + + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +func ecsClient(ctx context.Context) (*ecs.Client, error) { + cred, err := types.CredentialFromCtx(ctx) + if err != nil { + return nil, err + } + + cli, err := ecs.NewClientWithAccessKey(cred.Region, cred.AccessKey, cred.AccessSecret) + if err != nil { + return nil, fmt.Errorf("error create alibaba ecs client %s: %w", cred.AccessKey, err) + } + + cli.EnableAsync(10, 10) + + return cli, nil +} diff --git a/pkg/resourcehandlers/alibaba/resourcelog/common.go b/pkg/resourcehandlers/alibaba/resourcelog/common.go new file mode 100644 index 000000000..d575158b7 --- /dev/null +++ b/pkg/resourcehandlers/alibaba/resourcelog/common.go @@ -0,0 +1,3 @@ +package resourcelog + +const schemeHttps = "HTTPS" diff --git a/pkg/resourcehandlers/alibaba/resourcelog/ecs_instance.go b/pkg/resourcehandlers/alibaba/resourcelog/ecs_instance.go new file mode 100644 index 000000000..863eb9fd3 --- /dev/null +++ b/pkg/resourcehandlers/alibaba/resourcelog/ecs_instance.go @@ -0,0 +1,91 @@ +package resourcelog + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" + "github.com/seal-io/utils/stringx" + + "github.com/seal-io/walrus/pkg/resourcehandler" + optypes "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +const ( + fetchLogPeriod = 2 * time.Second +) + +type ecsInstance struct { + lastUpdatedTimestamp string + cli *ecs.Client + lastContent string +} + +func getEcsInstance(ctx context.Context) (optypes.LoggableResource, error) { + cli, err := ecsClient(ctx) + if err != nil { + return nil, err + } + + return &ecsInstance{ + cli: cli, + }, nil +} + +func (r *ecsInstance) Log(ctx context.Context, name string, opts resourcehandler.LogOptions) error { + for { + select { + case <-ctx.Done(): + return nil + default: + } + + req := ecs.CreateGetInstanceConsoleOutputRequest() + req.InstanceId = name + req.Scheme = schemeHttps + + resp, err := r.cli.GetInstanceConsoleOutput(req) + if err != nil { + return fmt.Errorf("error get console output for %s: %w", name, err) + } + + if resp.LastUpdateTime != "" { + if r.lastUpdatedTimestamp != "" && r.lastUpdatedTimestamp == resp.LastUpdateTime { + continue + } + r.lastUpdatedTimestamp = resp.LastUpdateTime + } + + if resp.ConsoleOutput != "" { + var content string + + // The console output is base64 encoded. + content, err := stringx.DecodeBase64(resp.ConsoleOutput) + if err != nil { + return err + } + + if r.lastContent != "" { + index := strings.LastIndex(content, r.lastContent) + if index > 0 { + content = content[index+1:] + } + } + + _, err = opts.Out.Write([]byte(content)) + if err != nil { + return fmt.Errorf("error write to output: %w", err) + } + + r.lastContent = content[len(content)-20:] + } + + if opts.WithoutFollow { + return nil + } + + time.Sleep(fetchLogPeriod) + } +} diff --git a/pkg/resourcehandlers/alibaba/resourcelog/registry.go b/pkg/resourcehandlers/alibaba/resourcelog/registry.go new file mode 100644 index 000000000..3dc2aa455 --- /dev/null +++ b/pkg/resourcehandlers/alibaba/resourcelog/registry.go @@ -0,0 +1,70 @@ +package resourcelog + +import ( + "context" + "errors" + + "k8s.io/klog/v2" + + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/resourcehandlers/alibaba/key" + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +// resourceTypes indicate supported resource type and their functions. +var resourceTypes map[string]getLoggableResource + +type getLoggableResource func(ctx context.Context) (types.LoggableResource, error) + +func init() { + resourceTypes = map[string]getLoggableResource{ + "alicloud_instance": getEcsInstance, + } +} + +// Supported indicate whether the resource is supported to get log. +func Supported(_ context.Context, k string) (bool, error) { + resourceType, _, ok := key.Decode(k) + if !ok { + return false, errors.New("invalid key") + } + + _, exist := resourceTypes[resourceType] + if !exist { + return false, nil + } + + return true, nil +} + +// Log get resource log by key. +func Log(ctx context.Context, k string, options resourcehandler.LogOptions) error { + supported, err := Supported(ctx, k) + if err != nil { + return err + } + + if !supported { + return errors.New("unsupported resource type") + } + + resourceType, name, ok := key.Decode(k) + if !ok { + return errors.New("invalid key") + } + + res := resourceTypes[resourceType] + + fs, err := res(ctx) + if err != nil { + return err + } + + err = fs.Log(ctx, name, options) + if err != nil { + klog.Warningf("error get log resource %s/%s: %v", resourceType, name, err) + return err + } + + return nil +} diff --git a/pkg/resourcehandlers/aws/key/key.go b/pkg/resourcehandlers/aws/key/key.go new file mode 100644 index 000000000..1cd1ca428 --- /dev/null +++ b/pkg/resourcehandlers/aws/key/key.go @@ -0,0 +1,26 @@ +package key + +import ( + "strings" + + "github.com/seal-io/utils/stringx" +) + +// Decode parses the given string into {resource type, resource name}, returns false if not a valid key. +func Decode(s string) (resourceType, name string, ok bool) { + ss := strings.SplitN(s, "/", 2) + + ok = len(ss) == 2 + if !ok { + return + } + resourceType = ss[0] + name = ss[1] + + return +} + +// Encode constructs the given {resource type, resource name} into a valid key. +func Encode(resourceType, name string) string { + return stringx.Join("/", resourceType, name) +} diff --git a/pkg/resourcehandlers/aws/resourceexec/ec2_instance.go b/pkg/resourcehandlers/aws/resourceexec/ec2_instance.go new file mode 100644 index 000000000..98f49a275 --- /dev/null +++ b/pkg/resourcehandlers/aws/resourceexec/ec2_instance.go @@ -0,0 +1,142 @@ +package resourceexec + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ssm" + ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/mmmorris1975/ssm-session-client/datachannel" + "github.com/seal-io/utils/pools/gopool" + + "github.com/seal-io/walrus/pkg/resourcehandler" + opawstypes "github.com/seal-io/walrus/pkg/resourcehandlers/aws/types" + optypes "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +const ( + defaultTerminalHeight uint16 = 100 + defaultTerminalWidth uint16 = 100 +) + +type ec2Instance struct { + awsCfg *aws.Config +} + +func getEc2Instance(ctx context.Context) (optypes.ExecutableResource, error) { + awsCfg, err := opawstypes.ConfigFromCtx(ctx) + if err != nil { + return nil, err + } + + return &ec2Instance{ + awsCfg: awsCfg, + }, nil +} + +// Supported check whether the session manager configured for current instance. +func (r *ec2Instance) Supported(ctx context.Context, name string) (bool, error) { + ssmCli := ssm.NewFromConfig(*r.awsCfg) + + req := &ssm.DescribeInstanceInformationInput{ + InstanceInformationFilterList: []ssmtypes.InstanceInformationFilter{ + { + Key: ssmtypes.InstanceInformationFilterKey("InstanceIds"), + ValueSet: []string{name}, + }, + }, + } + + resp, err := ssmCli.DescribeInstanceInformation(ctx, req) + if err != nil { + return false, err + } + + if len(resp.InstanceInformationList) == 0 || + resp.InstanceInformationList[0].PingStatus != ssmtypes.PingStatusOnline { + return false, nil + } + + return true, nil +} + +// Exec support read and write operations with the connection. +func (r *ec2Instance) Exec(ctx context.Context, name string, opts resourcehandler.ExecOptions) error { + c := new(datachannel.SsmDataChannel) + if err := c.Open(*r.awsCfg, &ssm.StartSessionInput{Target: aws.String(name)}); err != nil { + return err + } + defer c.Close() + + eg := gopool.Group() + eg.Go(func() error { + return r.setTerminalSize(ctx, opts, c) + }) + eg.Go(func() error { + select { + case <-ctx.Done(): + return nil + default: + _, err := io.Copy(c, opts.In) + if err != nil { + return fmt.Errorf("error write to remote: %w", err) + } + + return nil + } + }) + eg.Go(func() error { + select { + case <-ctx.Done(): + return nil + default: + _, err := io.Copy(opts.Out, c) + if err != nil { + return fmt.Errorf("error read from remote: %w", err) + } + + return nil + } + }) + + return eg.Wait() +} + +func (r *ec2Instance) setTerminalSize( + ctx context.Context, + opts resourcehandler.ExecOptions, + c *datachannel.SsmDataChannel, +) error { + // Without resizer. + if opts.Resizer == nil { + err := c.SetTerminalSize(uint32(defaultTerminalHeight), uint32(defaultTerminalWidth)) + if err != nil { + return fmt.Errorf("error set terminal size") + } + + return nil + } + + // With resizer. + for { + select { + case <-ctx.Done(): + return nil + default: + } + + width, height, ok := opts.Resizer.Next() + if !ok { + return errors.New("invalid terminal resizer") + } + + // Send resize data to remote connection. + err := c.SetTerminalSize(uint32(height), uint32(width)) + if err != nil { + return err + } + } +} diff --git a/pkg/resourcehandlers/aws/resourceexec/registry.go b/pkg/resourcehandlers/aws/resourceexec/registry.go new file mode 100644 index 000000000..16fbc082d --- /dev/null +++ b/pkg/resourcehandlers/aws/resourceexec/registry.go @@ -0,0 +1,83 @@ +package resourceexec + +import ( + "context" + "errors" + + "k8s.io/klog/v2" + + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/resourcehandlers/aws/key" + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +// resourceTypes indicate supported resource type and their functions. +var resourceTypes map[string]getExecutableResource + +type getExecutableResource func(ctx context.Context) (types.ExecutableResource, error) + +func init() { + resourceTypes = map[string]getExecutableResource{ + "aws_instance": getEc2Instance, + } +} + +// Supported indicate whether the resource is supported to exec. +func Supported(ctx context.Context, k string) (bool, error) { + resourceType, name, ok := key.Decode(k) + if !ok { + return false, errors.New("invalid key") + } + + fs, exist := resourceTypes[resourceType] + if !exist { + return false, nil + } + + res, err := fs(ctx) + if err != nil { + return false, err + } + + supported, err := res.Supported(ctx, name) + if err != nil { + return false, err + } + + if !supported { + return false, nil + } + + return supported, nil +} + +// Exec resource by key. +func Exec(ctx context.Context, k string, opts resourcehandler.ExecOptions) error { + supported, err := Supported(ctx, k) + if err != nil { + return err + } + + if !supported { + return errors.New("unsupported resource type") + } + + resourceType, name, ok := key.Decode(k) + if !ok { + return errors.New("invalid key") + } + + fs := resourceTypes[resourceType] + + res, err := fs(ctx) + if err != nil { + return err + } + + err = res.Exec(ctx, name, opts) + if err != nil { + klog.Warningf("error exec resource %s/%s: %v", resourceType, name, err) + } + + return err +} diff --git a/pkg/resourcehandlers/aws/resourcehandler.go b/pkg/resourcehandlers/aws/resourcehandler.go new file mode 100644 index 000000000..56af192e0 --- /dev/null +++ b/pkg/resourcehandlers/aws/resourcehandler.go @@ -0,0 +1,109 @@ +package aws + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/seal-io/utils/stringx" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/resourcehandlers/aws/resourceexec" + "github.com/seal-io/walrus/pkg/resourcehandlers/aws/resourcelog" + opawstypes "github.com/seal-io/walrus/pkg/resourcehandlers/aws/types" + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +const OperatorType = resourcehandler.ConnectorTypeAWS + +// New returns resourcehandlers.ResourceHandler with the given options. +func New(ctx context.Context, opts resourcehandler.CreateOptions) (resourcehandler.ResourceHandler, error) { + name := opts.Connector.Name + + config, err := types.GetConfigData(ctx, opts) + if err != nil { + return nil, err + } + + cred, err := types.GetCredential(config) + if err != nil { + return nil, err + } + + return Operator{ + name: name, + cred: cred, + identifier: stringx.SumBySHA256("aws:", cred.AccessKey, cred.AccessSecret), + }, nil +} + +type Operator struct { + name string + cred *types.Credential + identifier string +} + +func (op Operator) IsConnected(ctx context.Context) error { + cred := opawstypes.Credential(*op.cred) + + cfg, err := cred.Config() + if err != nil { + return err + } + + // Use DescribeRegions API to check reachable. + cli := ec2.NewFromConfig(*cfg) + + _, err = cli.DescribeRegions(ctx, nil) + if err != nil { + return fmt.Errorf("error connect to aws: %w", err) + } + + return nil +} + +func (op Operator) Type() resourcehandler.Type { + return OperatorType +} + +// Burst implements resourcehandlers.ResourceHandler. +func (op Operator) Burst() int { + return 200 +} + +// ID implements resourcehandlers.ResourceHandler. +func (op Operator) ID() string { + return op.identifier +} + +// GetComponents implements resourcehandlers.ResourceHandler. +func (op Operator) GetComponents( + ctx context.Context, + resource *walrus.ResourceComponents, +) ([]*walrus.ResourceComponents, error) { + return nil, nil +} + +// Log implements resourcehandlers.ResourceHandler. +func (op Operator) Log(ctx context.Context, key string, opts resourcehandler.LogOptions) error { + newCtx := context.WithValue(ctx, types.CredentialKey, op.cred) + return resourcelog.Log(newCtx, key, opts) +} + +// Exec implements resourcehandlers.ResourceHandler. +func (op Operator) Exec(ctx context.Context, key string, opts resourcehandler.ExecOptions) error { + newCtx := context.WithValue(ctx, types.CredentialKey, op.cred) + return resourceexec.Exec(newCtx, key, opts) +} + +func (op Operator) GetKeys(ctx context.Context, component *walrus.ResourceComponents) (*resourcehandler.ResourceComponentOperationKeys, error) { + return nil, nil +} + +func (op Operator) GetStatus(ctx context.Context, component *walrus.ResourceComponents) ([]meta.Condition, error) { + // TODO: Implement this method after resource is migrated. + + return nil, nil +} diff --git a/pkg/resourcehandlers/aws/resourcelog/client.go b/pkg/resourcehandlers/aws/resourcelog/client.go new file mode 100644 index 000000000..51f221953 --- /dev/null +++ b/pkg/resourcehandlers/aws/resourcelog/client.go @@ -0,0 +1,18 @@ +package resourcelog + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + + opawstypes "github.com/seal-io/walrus/pkg/resourcehandlers/aws/types" +) + +func ec2Client(ctx context.Context) (*ec2.Client, error) { + cfg, err := opawstypes.ConfigFromCtx(ctx) + if err != nil { + return nil, err + } + + return ec2.NewFromConfig(*cfg), nil +} diff --git a/pkg/resourcehandlers/aws/resourcelog/ec2_instance.go b/pkg/resourcehandlers/aws/resourcelog/ec2_instance.go new file mode 100644 index 000000000..9c8732fcb --- /dev/null +++ b/pkg/resourcehandlers/aws/resourcelog/ec2_instance.go @@ -0,0 +1,89 @@ +package resourcelog + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/seal-io/utils/stringx" + + "github.com/seal-io/walrus/pkg/resourcehandler" + optypes "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +const ( + fetchLogPeriod = 2 * time.Second +) + +type ec2Instance struct { + lastUpdatedTimestamp *time.Time + cli *ec2.Client + lastContent string +} + +func getEc2Instance(ctx context.Context) (optypes.LoggableResource, error) { + cli, err := ec2Client(ctx) + if err != nil { + return nil, err + } + + return &ec2Instance{ + cli: cli, + }, nil +} + +func (r *ec2Instance) Log(ctx context.Context, name string, opts resourcehandler.LogOptions) error { + for { + select { + case <-ctx.Done(): + return nil + default: + } + + resp, err := r.cli.GetConsoleOutput(ctx, &ec2.GetConsoleOutputInput{ + InstanceId: &name, + }) + if err != nil { + return fmt.Errorf("error get instance output: %w", err) + } + + if resp.Timestamp != nil { + if r.lastUpdatedTimestamp != nil && r.lastUpdatedTimestamp.Equal(*resp.Timestamp) { + continue + } + r.lastUpdatedTimestamp = resp.Timestamp + } + + if resp.Output != nil { + var content string + + // The console output is base64 encoded. + content, err := stringx.DecodeBase64(*resp.Output) + if err != nil { + return err + } + + if r.lastContent != "" { + index := strings.LastIndex(content, r.lastContent) + if index > 0 { + content = content[index+1:] + } + } + + _, err = opts.Out.Write([]byte(content)) + if err != nil { + return fmt.Errorf("error write to output: %w", err) + } + + r.lastContent = content[len(content)-20:] + } + + if opts.WithoutFollow { + return nil + } + + time.Sleep(fetchLogPeriod) + } +} diff --git a/pkg/resourcehandlers/aws/resourcelog/registry.go b/pkg/resourcehandlers/aws/resourcelog/registry.go new file mode 100644 index 000000000..9784ae851 --- /dev/null +++ b/pkg/resourcehandlers/aws/resourcelog/registry.go @@ -0,0 +1,69 @@ +package resourcelog + +import ( + "context" + "errors" + + "k8s.io/klog/v2" + + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/resourcehandlers/aws/key" + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +// resourceTypes indicate supported resource type and their functions. +var resourceTypes map[string]getLoggableResource + +type getLoggableResource func(ctx context.Context) (types.LoggableResource, error) + +func init() { + resourceTypes = map[string]getLoggableResource{ + "aws_instance": getEc2Instance, + } +} + +// Supported indicate whether the resource is supported to get log. +func Supported(_ context.Context, k string) (bool, error) { + resourceType, _, ok := key.Decode(k) + if !ok { + return false, errors.New("invalid key") + } + + _, exist := resourceTypes[resourceType] + if !exist { + return false, nil + } + + return true, nil +} + +// Log get resource log by key. +func Log(ctx context.Context, k string, options resourcehandler.LogOptions) error { + supported, err := Supported(ctx, k) + if err != nil { + return err + } + + if !supported { + return errors.New("unsupported resource type") + } + + resourceType, name, ok := key.Decode(k) + if !ok { + return errors.New("invalid key") + } + + res := resourceTypes[resourceType] + + fs, err := res(ctx) + if err != nil { + return err + } + + err = fs.Log(ctx, name, options) + if err != nil { + klog.Warningf("error get log resource %s/%s: %v", resourceType, name, err) + } + + return nil +} diff --git a/pkg/resourcehandlers/aws/types/credential.go b/pkg/resourcehandlers/aws/types/credential.go new file mode 100644 index 000000000..263dd1699 --- /dev/null +++ b/pkg/resourcehandlers/aws/types/credential.go @@ -0,0 +1,49 @@ +package types + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +// Credential is a struct use to implement aws credentials. +type Credential types.Credential + +// Retrieve implement aws.CredentialsProvider interface. +func (a Credential) Retrieve(_ context.Context) (aws.Credentials, error) { + return aws.Credentials{ + AccessKeyID: a.AccessKey, + SecretAccessKey: a.AccessSecret, + }, nil +} + +// Config creates an AWS SDK V2 Config. +func (a Credential) Config() (*aws.Config, error) { + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithCredentialsProvider(a), config.WithRegion(a.Region)) + if err != nil { + return &cfg, + fmt.Errorf( + "error create aws config with accessKey %s region %s: %w", + a.AccessKey, + a.Region, + err, + ) + } + + return &cfg, nil +} + +// ConfigFromCtx get credential from context and creates an AWS SDK V2 Config. +func ConfigFromCtx(ctx context.Context) (*aws.Config, error) { + cred, err := types.CredentialFromCtx(ctx) + if err != nil { + return nil, err + } + wrap := Credential(*cred) + + return wrap.Config() +} diff --git a/pkg/resourcehandlers/azure/resourcehandler.go b/pkg/resourcehandlers/azure/resourcehandler.go new file mode 100644 index 000000000..ae3b524d6 --- /dev/null +++ b/pkg/resourcehandlers/azure/resourcehandler.go @@ -0,0 +1,108 @@ +package azure + +import ( + "context" + "fmt" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" + "github.com/seal-io/utils/stringx" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + "github.com/seal-io/walrus/pkg/resourcehandler" + aztypes "github.com/seal-io/walrus/pkg/resourcehandlers/azure/types" + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +const OperatorType = resourcehandler.ConnectorTypeAzure + +// New returns resourcehandlers.ResourceHandler with the given options. +func New(ctx context.Context, opts resourcehandler.CreateOptions) (resourcehandler.ResourceHandler, error) { + name := opts.Connector.Name + config, err := types.GetConfigData(ctx, opts) + if err != nil { + return nil, err + } + + cred, err := aztypes.GetCredential(config) + if err != nil { + return nil, err + } + + return Operator{ + name: name, + cred: cred, + identifier: stringx.SumBySHA256("azure:", cred.SubscriptionID, cred.TenantID, cred.ClientID), + }, nil +} + +type Operator struct { + name string + cred *aztypes.Credential + identifier string +} + +func (o Operator) Type() resourcehandler.Type { + return OperatorType +} + +func (o Operator) IsConnected(ctx context.Context) error { + cred, err := azidentity.NewClientSecretCredential(o.cred.TenantID, o.cred.ClientID, o.cred.ClientSecret, nil) + if err != nil { + return err + } + + clientFactory, err := armresources.NewClientFactory(o.cred.SubscriptionID, cred, nil) + if err != nil { + return err + } + + client := clientFactory.NewResourceGroupsClient() + + pager := client.NewListPager(nil) + + _, err = pager.NextPage(ctx) + if err != nil { + return fmt.Errorf("error connect to azure: %w", err) + } + + return nil +} + +func (o Operator) Burst() int { + return 100 +} + +// ID implements resourcehandlers.ResourceHandler. +func (o Operator) ID() string { + return o.identifier +} + +// GetComponents implements resourcehandlers.ResourceHandler. +func (o Operator) GetComponents( + ctx context.Context, + resource *walrus.ResourceComponents, +) ([]*walrus.ResourceComponents, error) { + return nil, nil +} + +// Log implements resourcehandlers.ResourceHandler. +func (o Operator) Log(ctx context.Context, key string, opts resourcehandler.LogOptions) error { + return nil +} + +// Exec implements resourcehandlers.ResourceHandler. +func (o Operator) Exec(ctx context.Context, key string, opts resourcehandler.ExecOptions) error { + return nil +} + +func (o Operator) GetKeys(ctx context.Context, component *walrus.ResourceComponents) (*resourcehandler.ResourceComponentOperationKeys, error) { + return nil, nil +} + +func (o Operator) GetStatus(ctx context.Context, component *walrus.ResourceComponents) ([]meta.Condition, error) { + // TODO: Implement this method after resource is migrated. + + return nil, nil +} diff --git a/pkg/resourcehandlers/azure/types/credential.go b/pkg/resourcehandlers/azure/types/credential.go new file mode 100644 index 000000000..749c86c22 --- /dev/null +++ b/pkg/resourcehandlers/azure/types/credential.go @@ -0,0 +1,45 @@ +package types + +import ( + "errors" +) + +const ( + SubscriptionID = "subscription_id" + TenantID = "tenant_id" + ClientID = "client_id" + ClientSecret = "client_secret" +) + +type Credential struct { + SubscriptionID string + TenantID string + ClientID string + ClientSecret string +} + +func GetCredential(configData map[string][]byte) (*Credential, error) { + cred := &Credential{} + + cred.SubscriptionID = string(configData[SubscriptionID]) + if cred.SubscriptionID == "" { + return nil, errors.New("subscriptionID is empty") + } + + cred.TenantID = string(configData[TenantID]) + if cred.TenantID == "" { + return nil, errors.New("tenantID is empty") + } + + cred.ClientID = string(configData[ClientID]) + if cred.ClientID == "" { + return nil, errors.New("clientID is empty") + } + + cred.ClientSecret = string(configData[ClientSecret]) + if cred.ClientSecret == "" { + return nil, errors.New("clientSecret is empty") + } + + return cred, nil +} diff --git a/pkg/resourcehandlers/docker/resourcehandler.go b/pkg/resourcehandlers/docker/resourcehandler.go new file mode 100644 index 000000000..3482d661f --- /dev/null +++ b/pkg/resourcehandlers/docker/resourcehandler.go @@ -0,0 +1,96 @@ +package docker + +import ( + "context" + "errors" + "fmt" + + "github.com/docker/docker/client" + "github.com/seal-io/utils/stringx" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + "github.com/seal-io/walrus/pkg/resourcehandler" + optypes "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +const OperatorType = resourcehandler.ConnectorTypeDocker + +// New returns resourcehandlers.ResourceHandler with the given options. +func New(ctx context.Context, opts resourcehandler.CreateOptions) (resourcehandler.ResourceHandler, error) { + name := opts.Connector.Name + config, err := optypes.GetConfigData(ctx, opts) + if err != nil { + return nil, err + } + + host := string(config["host"]) + if host == "" { + return nil, errors.New("host is empty") + } + + cli, err := client.NewClientWithOpts(client.WithAPIVersionNegotiation()) + if err != nil { + return nil, err + } + + return Operator{ + name: name, + identifier: stringx.SumBySHA256("docker:", host), + client: cli, + }, nil +} + +type Operator struct { + name string + identifier string + client *client.Client +} + +func (op Operator) Type() resourcehandler.Type { + return OperatorType +} + +func (op Operator) IsConnected(ctx context.Context) error { + if _, err := op.client.ServerVersion(ctx); err != nil { + return fmt.Errorf("error connect to docker daemon: %w", err) + } + + return nil +} + +func (op Operator) Burst() int { + return 100 +} + +func (op Operator) ID() string { + return op.identifier +} + +// GetComponents implements resourcehandlers.ResourceHandler. +func (op Operator) GetComponents( + ctx context.Context, + resource *walrus.ResourceComponents, +) ([]*walrus.ResourceComponents, error) { + return nil, nil +} + +// Log implements resourcehandlers.ResourceHandler. +func (op Operator) Log(ctx context.Context, key string, opts resourcehandler.LogOptions) error { + return nil +} + +// Exec implements resourcehandlers.ResourceHandler. +func (op Operator) Exec(ctx context.Context, key string, opts resourcehandler.ExecOptions) error { + return nil +} + +func (op Operator) GetKeys(ctx context.Context, component *walrus.ResourceComponents) (*resourcehandler.ResourceComponentOperationKeys, error) { + return nil, nil +} + +func (op Operator) GetStatus(ctx context.Context, component *walrus.ResourceComponents) ([]meta.Condition, error) { + // TODO: Implement this method after resource is migrated. + + return nil, nil +} diff --git a/pkg/resourcehandlers/google/resourcehandler.go b/pkg/resourcehandlers/google/resourcehandler.go new file mode 100644 index 000000000..917038ffc --- /dev/null +++ b/pkg/resourcehandlers/google/resourcehandler.go @@ -0,0 +1,98 @@ +package google + +import ( + "context" + "fmt" + + "github.com/seal-io/utils/stringx" + "google.golang.org/api/compute/v1" + "google.golang.org/api/option" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + "github.com/seal-io/walrus/pkg/resourcehandler" + gtypes "github.com/seal-io/walrus/pkg/resourcehandlers/google/types" + "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +const OperatorType = resourcehandler.ConnectorTypeGoogle + +// New returns resourcehandlers.ResourceHandler with the given options. +func New(ctx context.Context, opts resourcehandler.CreateOptions) (resourcehandler.ResourceHandler, error) { + name := opts.Connector.Name + config, err := types.GetConfigData(ctx, opts) + if err != nil { + return nil, err + } + + cred, err := gtypes.GetCredential(config) + if err != nil { + return nil, err + } + + return Operator{ + name: name, + cred: cred, + identifier: stringx.SumBySHA256("google:", cred.Project, cred.Region, cred.Zone), + }, nil +} + +type Operator struct { + name string + cred *gtypes.Credential + identifier string +} + +func (op Operator) Type() resourcehandler.Type { + return OperatorType +} + +func (op Operator) IsConnected(ctx context.Context) error { + service, err := compute.NewService(ctx, option.WithCredentialsJSON([]byte(op.cred.Credentials))) + if err != nil { + return err + } + + _, err = service.Regions.List(op.cred.Project).Do() + if err != nil { + return fmt.Errorf("error connect to google cloud: %w", err) + } + + return nil +} + +func (op Operator) Burst() int { + return 100 +} + +func (op Operator) ID() string { + return op.identifier +} + +// GetComponents implements resourcehandlers.ResourceHandler. +func (op Operator) GetComponents( + ctx context.Context, + resource *walrus.ResourceComponents, +) ([]*walrus.ResourceComponents, error) { + return nil, nil +} + +// Log implements resourcehandlers.ResourceHandler. +func (op Operator) Log(ctx context.Context, key string, opts resourcehandler.LogOptions) error { + return nil +} + +// Exec implements resourcehandlers.ResourceHandler. +func (op Operator) Exec(ctx context.Context, key string, opts resourcehandler.ExecOptions) error { + return nil +} + +func (op Operator) GetKeys(ctx context.Context, component *walrus.ResourceComponents) (*resourcehandler.ResourceComponentOperationKeys, error) { + return nil, nil +} + +func (op Operator) GetStatus(ctx context.Context, component *walrus.ResourceComponents) ([]meta.Condition, error) { + // TODO: Implement this method after resource is migrated. + + return nil, nil +} diff --git a/pkg/resourcehandlers/google/types/credential.go b/pkg/resourcehandlers/google/types/credential.go new file mode 100644 index 000000000..281591562 --- /dev/null +++ b/pkg/resourcehandlers/google/types/credential.go @@ -0,0 +1,45 @@ +package types + +import ( + "errors" +) + +const ( + Project = "project" + Region = "region" + Zone = "zone" + Credentials = "credentials" +) + +type Credential struct { + Project string + Region string + Zone string + Credentials string +} + +func GetCredential(configData map[string][]byte) (*Credential, error) { + cred := &Credential{} + + cred.Project = string(configData[Project]) + if cred.Project == "" { + return nil, errors.New("project is empty") + } + + cred.Region = string(configData[Region]) + if cred.Region == "" { + return nil, errors.New("region is empty") + } + + cred.Zone = string(configData[Zone]) + if cred.Zone == "" { + return nil, errors.New("zone is empty") + } + + cred.Credentials = string(configData[Credentials]) + if cred.Credentials == "" { + return nil, errors.New("credentials is empty") + } + + return cred, nil +} diff --git a/pkg/resourcehandlers/k8s/driver.go b/pkg/resourcehandlers/k8s/driver.go new file mode 100644 index 000000000..42e4d045c --- /dev/null +++ b/pkg/resourcehandlers/k8s/driver.go @@ -0,0 +1,101 @@ +package k8s + +import ( + "context" + "fmt" + "time" + + "github.com/seal-io/utils/stringx" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "github.com/seal-io/walrus/pkg/resourcehandler" + optypes "github.com/seal-io/walrus/pkg/resourcehandlers/types" +) + +// GetConfig returns the rest.Config with the given model, +// by default, the rest.Config configures with 15s timeout/16 qps/64 burst, +// please modify the default configuration with ConfigOption as need. +func GetConfig(ctx context.Context, createOpts resourcehandler.CreateOptions, opts ...func(*rest.Config)) (restConfig *rest.Config, err error) { + apiConfig, _, err := LoadApiConfig(ctx, createOpts) + if err != nil { + return nil, err + } + + restConfig, err = clientcmd. + NewNonInteractiveClientConfig(*apiConfig, "", &clientcmd.ConfigOverrides{}, nil). + ClientConfig() + if err != nil { + err = fmt.Errorf("cannot construct rest config from api config: %w", err) + return + } + restConfig.Timeout = 15 * time.Second + + for _, opt := range opts { + opt(restConfig) + } + + return +} + +// LoadApiConfig returns the api.Config with the given model. +func LoadApiConfig(ctx context.Context, opts resourcehandler.CreateOptions) (apiConfig *clientcmdapi.Config, raw string, err error) { + con := opts.Connector + version := con.Spec.Config.Version + + switch version { + default: + return nil, "", fmt.Errorf("unknown config version: %v", version) + case "v1": + config, err := optypes.GetConfigData(ctx, opts) + if err != nil { + return nil, "", fmt.Errorf("error get config data: %w", err) + } + + // { + // "configVersion": "v1", + // "configData": { + // "kubeconfig": "..." + // } + // }. + raw, err = loadRawConfigV1(config) + if err != nil { + return nil, "", fmt.Errorf("error load config from connector %s: %w", con.Name, err) + } + + apiConfig, err = loadApiConfigV1(raw) + if err != nil { + return nil, "", fmt.Errorf("error load version %s config: %w", version, err) + } + } + + return +} + +func loadRawConfigV1(data map[string][]byte) (string, error) { + // { + // "kubeconfig": "..." + // }. + kubeconfigText, ok := data["kubeconfig"] + + if !ok { + return "", fmt.Errorf(`failed to get "kubeconfig"`) + } + + return string(kubeconfigText), nil +} + +func loadApiConfigV1(kubeconfigText string) (*clientcmdapi.Config, error) { + config, err := clientcmd.Load(stringx.ToBytes(&kubeconfigText)) + if err != nil { + return nil, fmt.Errorf("error load api config: %w", err) + } + + err = clientcmd.Validate(*config) + if err != nil { + return nil, fmt.Errorf("error validate api config: %w", err) + } + + return config, nil +} diff --git a/pkg/resourcehandlers/k8s/helper.go b/pkg/resourcehandlers/k8s/helper.go new file mode 100644 index 000000000..65fc82b0d --- /dev/null +++ b/pkg/resourcehandlers/k8s/helper.go @@ -0,0 +1,33 @@ +package k8s + +import ( + "context" + "fmt" + + "github.com/seal-io/utils/json" + "k8s.io/client-go/rest" +) + +func IsConnected(ctx context.Context, r rest.Interface) error { + body, err := r.Get(). + AbsPath("/version"). + Do(ctx). + Raw() + if err != nil { + return err + } + + var info struct { + Major string `json:"major"` + Minor string `json:"minor"` + Compiler string `json:"compiler"` + Platform string `json:"platform"` + } + + err = json.Unmarshal(body, &info) + if err != nil { + return fmt.Errorf("unable to parse the server version: %w", err) + } + + return nil +} diff --git a/pkg/resourcehandlers/k8s/intercept/converter_tf.go b/pkg/resourcehandlers/k8s/intercept/converter_tf.go new file mode 100644 index 000000000..e0ab2213e --- /dev/null +++ b/pkg/resourcehandlers/k8s/intercept/converter_tf.go @@ -0,0 +1,171 @@ +package intercept + +import ( + "fmt" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" + appsv1 "k8s.io/api/apps/v1" + autoscalingv1 "k8s.io/api/autoscaling/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2" + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + certificatesv1 "k8s.io/api/certificates/v1" + certificatesv1beta1 "k8s.io/api/certificates/v1beta1" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + policyv1beta1 "k8s.io/api/policy/v1beta1" + rbacv1 "k8s.io/api/rbac/v1" + schedulingv1 "k8s.io/api/scheduling/v1" + storagev1 "k8s.io/api/storage/v1" + storagev1beta1 "k8s.io/api/storage/v1beta1" + kmeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" +) + +func init() { + // Emit, transfer and record. + // + // ref to https://registry.terraform.io/providers/hashicorp/kubernetes/2.18.1. + // + for _, alias := range TFAllTypes { + gvk := func() schema.GroupVersionKind { + switch alias { + case "kubernetes_namespace_v1", "kubernetes_namespace": + return corev1.SchemeGroupVersion.WithKind("Namespace") + case "kubernetes_service_v1", "kubernetes_service": + return corev1.SchemeGroupVersion.WithKind("Service") + case "kubernetes_service_account_v1", + "kubernetes_service_account", + "kubernetes_default_service_account_v1", + "kubernetes_default_service_account": + return corev1.SchemeGroupVersion.WithKind("ServiceAccount") + case "kubernetes_config_map_v1", "kubernetes_config_map", "kubernetes_config_map_v1_data": + return corev1.SchemeGroupVersion.WithKind("ConfigMap") + case "kubernetes_secret_v1", "kubernetes_secret": + return corev1.SchemeGroupVersion.WithKind("Secret") + case "kubernetes_pod_v1", "kubernetes_pod": + return corev1.SchemeGroupVersion.WithKind("Pod") + case "kubernetes_endpoints_v1", "kubernetes_endpoints": + return corev1.SchemeGroupVersion.WithKind("Endpoints") + case "kubernetes_limit_range_v1", "kubernetes_limit_range": + return corev1.SchemeGroupVersion.WithKind("LimitRange") + case "kubernetes_persistent_volume_v1", "kubernetes_persistent_volume": + return corev1.SchemeGroupVersion.WithKind("PersistentVolume") + case "kubernetes_persistent_volume_claim_v1", "kubernetes_persistent_volume_claim": + return corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim") + case "kubernetes_replication_controller_v1", "kubernetes_replication_controller": + return corev1.SchemeGroupVersion.WithKind("ReplicationController") + case "kubernetes_resource_quota_v1", "kubernetes_resource_quota": + return corev1.SchemeGroupVersion.WithKind("ResourceQuota") + + case "kubernetes_api_service_v1", "kubernetes_api_service": + return apiregistrationv1.SchemeGroupVersion.WithKind("APIService") + + case "kubernetes_deployment_v1", "kubernetes_deployment": + return appsv1.SchemeGroupVersion.WithKind("Deployment") + case "kubernetes_daemon_set_v1", "kubernetes_daemonset", "kubernetes_daemon_set": + return appsv1.SchemeGroupVersion.WithKind("DaemonSet") + case "kubernetes_stateful_set_v1", "kubernetes_stateful_set": + return appsv1.SchemeGroupVersion.WithKind("StatefulSet") + + case "kubernetes_cron_job_v1": + return batchv1.SchemeGroupVersion.WithKind("CronJob") + case "kubernetes_cron_job": + return batchv1beta1.SchemeGroupVersion.WithKind("CronJob") + case "kubernetes_job_v1", "kubernetes_job": + return batchv1.SchemeGroupVersion.WithKind("Job") + + case "kubernetes_horizontal_pod_autoscaler_v2": + return autoscalingv2.SchemeGroupVersion.WithKind("HorizontalPodAutoscaler") + case "kubernetes_horizontal_pod_autoscaler_v2beta2": + return autoscalingv2beta2.SchemeGroupVersion.WithKind("HorizontalPodAutoscaler") + case "kubernetes_horizontal_pod_autoscaler_v1", "kubernetes_horizontal_pod_autoscaler": + return autoscalingv1.SchemeGroupVersion.WithKind("HorizontalPodAutoscaler") + + case "kubernetes_certificate_signing_request_v1": + return certificatesv1.SchemeGroupVersion.WithKind("CertificateSigningRequest") + case "kubernetes_certificate_signing_request": + return certificatesv1beta1.SchemeGroupVersion.WithKind("CertificateSigningRequest") + + case "kubernetes_role_v1", "kubernetes_role": + return rbacv1.SchemeGroupVersion.WithKind("Role") + case "kubernetes_role_binding_v1", "kubernetes_role_binding": + return rbacv1.SchemeGroupVersion.WithKind("RoleBinding") + case "kubernetes_cluster_role_v1", "kubernetes_cluster_role": + return rbacv1.SchemeGroupVersion.WithKind("ClusterRole") + case "kubernetes_cluster_role_binding_v1", "kubernetes_cluster_role_binding": + return rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding") + + case "kubernetes_ingress_v1": + return networkingv1.SchemeGroupVersion.WithKind("Ingress") + case "kubernetes_ingress": + return extensionsv1beta1.SchemeGroupVersion.WithKind("Ingress") + case "kubernetes_ingress_class_v1", "kubernetes_ingress_class": + return networkingv1.SchemeGroupVersion.WithKind("IngressClass") + case "kubernetes_network_policy_v1", "kubernetes_network_policy": + return networkingv1.SchemeGroupVersion.WithKind("NetworkPolicy") + + case "kubernetes_pod_disruption_budget_v1": + return policyv1.SchemeGroupVersion.WithKind("PodDisruptionBudget") + case "kubernetes_pod_disruption_budget": + return policyv1beta1.SchemeGroupVersion.WithKind("PodDisruptionBudget") + case "kubernetes_pod_security_policy_v1beta1", "kubernetes_pod_security_policy": + return policyv1beta1.SchemeGroupVersion.WithKind("PodSecurityPolicy") + + case "kubernetes_priority_class_v1", "kubernetes_priority_class": + return schedulingv1.SchemeGroupVersion.WithKind("PriorityClass") + + case "kubernetes_validating_webhook_configuration_v1": + return admissionregistrationv1.SchemeGroupVersion.WithKind("ValidatingWebhookConfiguration") + case "kubernetes_validating_webhook_configuration": + return admissionregistrationv1beta1.SchemeGroupVersion.WithKind("ValidatingWebhookConfiguration") + case "kubernetes_mutating_webhook_configuration_v1": + return admissionregistrationv1.SchemeGroupVersion.WithKind("MutatingWebhookConfiguration") + case "kubernetes_mutating_webhook_configuration": + return admissionregistrationv1beta1.SchemeGroupVersion.WithKind("MutatingWebhookConfiguration") + + case "kubernetes_storage_class_v1", "kubernetes_storage_class": + return storagev1.SchemeGroupVersion.WithKind("StorageClass") + case "kubernetes_csi_driver_v1": + return storagev1.SchemeGroupVersion.WithKind("CSIDriver") + case "kubernetes_csi_driver": + return storagev1beta1.SchemeGroupVersion.WithKind("CSIDriver") + } + + panic(fmt.Sprintf("needs transferring new alias %s to Kubernetes GVK", alias)) + }() + tfConvert.gvkm[alias] = gvk + tfConvert.gvrm[alias], _ = kmeta.UnsafeGuessKindToResource(gvk) + } +} + +// Terraform returns Converter to convert Terraform provider resource type to raw Kubernetes GVK/GVR. +func Terraform() Converter { + // Singleton pattern. + return tfConvert +} + +type terraformConvert struct { + gvkm map[string]schema.GroupVersionKind + gvrm map[string]schema.GroupVersionResource +} + +func (c terraformConvert) GetGVK(alias string) (gvk schema.GroupVersionKind, ok bool) { + gvk, ok = c.gvkm[alias] + return +} + +func (c terraformConvert) GetGVR(alias string) (gvr schema.GroupVersionResource, ok bool) { + gvr, ok = c.gvrm[alias] + return +} + +var tfConvert = terraformConvert{ + gvkm: map[string]schema.GroupVersionKind{}, + gvrm: map[string]schema.GroupVersionResource{}, +} diff --git a/pkg/resourcehandlers/k8s/intercept/enforcer_accessible.go b/pkg/resourcehandlers/k8s/intercept/enforcer_accessible.go new file mode 100644 index 000000000..e7ff71863 --- /dev/null +++ b/pkg/resourcehandlers/k8s/intercept/enforcer_accessible.go @@ -0,0 +1,51 @@ +package intercept + +import ( + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + networkingv1 "k8s.io/api/networking/v1" + kmeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" +) + +func init() { + // Emit, transfer and record. + // + // Only consider accessible types. + // + for _, gvk := range []schema.GroupVersionKind{ + corev1.SchemeGroupVersion.WithKind("Service"), + networkingv1.SchemeGroupVersion.WithKind("Ingress"), + extensionsv1beta1.SchemeGroupVersion.WithKind("Ingress"), + } { + acEnforcer.gvks.Insert(gvk) + gvr, _ := kmeta.UnsafeGuessKindToResource(gvk) + acEnforcer.gvrs.Insert(gvr) + } +} + +// Accessible returns Enforcer to detect if the given Kubernetes GVK/GVR is accessible enforcer. +func Accessible() Enforcer { + // Singleton pattern. + return acEnforcer +} + +// accessibleEnforcer implements Enforcer. +type accessibleEnforcer struct { + gvks sets.Set[schema.GroupVersionKind] + gvrs sets.Set[schema.GroupVersionResource] +} + +func (e accessibleEnforcer) AllowGVK(gvk schema.GroupVersionKind) bool { + return e.gvks.Has(gvk) +} + +func (e accessibleEnforcer) AllowGVR(gvr schema.GroupVersionResource) bool { + return e.gvrs.Has(gvr) +} + +var acEnforcer = accessibleEnforcer{ + gvks: sets.Set[schema.GroupVersionKind]{}, + gvrs: sets.Set[schema.GroupVersionResource]{}, +} diff --git a/pkg/resourcehandlers/k8s/intercept/enforcer_composite.go b/pkg/resourcehandlers/k8s/intercept/enforcer_composite.go new file mode 100644 index 000000000..25f81d2b4 --- /dev/null +++ b/pkg/resourcehandlers/k8s/intercept/enforcer_composite.go @@ -0,0 +1,64 @@ +package intercept + +import ( + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + corev1 "k8s.io/api/core/v1" + kmeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" +) + +func init() { + // Emit, transfer and record. + // + // Only consider composite types. + // + for _, gvk := range []schema.GroupVersionKind{ + // Select generated job list by the cronjob label selector, + // then select pod list by the job label selector. + batchv1.SchemeGroupVersion.WithKind("CronJob"), + batchv1beta1.SchemeGroupVersion.WithKind("CronJob"), + + // Select related persistent volume by the persistent volume claim. + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"), + + // Select generated pod list by the following kinds' label selector. + appsv1.SchemeGroupVersion.WithKind("DaemonSet"), + appsv1.SchemeGroupVersion.WithKind("Deployment"), + appsv1.SchemeGroupVersion.WithKind("StatefulSet"), + appsv1.SchemeGroupVersion.WithKind("ReplicaSet"), + batchv1.SchemeGroupVersion.WithKind("Job"), + corev1.SchemeGroupVersion.WithKind("ReplicationController"), + } { + compEnforcer.gvks.Insert(gvk) + gvr, _ := kmeta.UnsafeGuessKindToResource(gvk) + compEnforcer.gvrs.Insert(gvr) + } +} + +// Composite returns Enforcer to detect if the given Kubernetes GVK/GVR is composite enforcer. +func Composite() Enforcer { + // Singleton pattern. + return compEnforcer +} + +// compositeEnforcer implements Enforcer. +type compositeEnforcer struct { + gvks sets.Set[schema.GroupVersionKind] + gvrs sets.Set[schema.GroupVersionResource] +} + +func (e compositeEnforcer) AllowGVK(gvk schema.GroupVersionKind) bool { + return e.gvks.Has(gvk) +} + +func (e compositeEnforcer) AllowGVR(gvr schema.GroupVersionResource) bool { + return e.gvrs.Has(gvr) +} + +var compEnforcer = compositeEnforcer{ + gvks: sets.Set[schema.GroupVersionKind]{}, + gvrs: sets.Set[schema.GroupVersionResource]{}, +} diff --git a/pkg/resourcehandlers/k8s/intercept/enforcer_operable.go b/pkg/resourcehandlers/k8s/intercept/enforcer_operable.go new file mode 100644 index 000000000..84b292e09 --- /dev/null +++ b/pkg/resourcehandlers/k8s/intercept/enforcer_operable.go @@ -0,0 +1,48 @@ +package intercept + +import ( + corev1 "k8s.io/api/core/v1" + kmeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" +) + +func init() { + // Emit, transfer and record. + // + // Only consider operable types. + // + for _, gvk := range []schema.GroupVersionKind{ + // Select pod directly. + corev1.SchemeGroupVersion.WithKind("Pod"), + } { + opEnforcer.gvks.Insert(gvk) + gvr, _ := kmeta.UnsafeGuessKindToResource(gvk) + opEnforcer.gvrs.Insert(gvr) + } +} + +// Operable returns Enforcer to detect if the given Kubernetes GVK/GVR is operable enforcer. +func Operable() Enforcer { + // Singleton pattern. + return opEnforcer +} + +// operableEnforcer implements Enforcer. +type operableEnforcer struct { + gvks sets.Set[schema.GroupVersionKind] + gvrs sets.Set[schema.GroupVersionResource] +} + +func (e operableEnforcer) AllowGVK(gvk schema.GroupVersionKind) bool { + return e.gvks.Has(gvk) +} + +func (e operableEnforcer) AllowGVR(gvr schema.GroupVersionResource) bool { + return e.gvrs.Has(gvr) +} + +var opEnforcer = operableEnforcer{ + gvks: sets.Set[schema.GroupVersionKind]{}, + gvrs: sets.Set[schema.GroupVersionResource]{}, +} diff --git a/pkg/resourcehandlers/k8s/intercept/tf_types.go b/pkg/resourcehandlers/k8s/intercept/tf_types.go new file mode 100644 index 000000000..02aa9adf2 --- /dev/null +++ b/pkg/resourcehandlers/k8s/intercept/tf_types.go @@ -0,0 +1,111 @@ +package intercept + +var TFAllTypes = []string{ + // Core. + "kubernetes_namespace_v1", "kubernetes_namespace", + "kubernetes_service_v1", "kubernetes_service", + "kubernetes_service_account_v1", "kubernetes_service_account", + "kubernetes_default_service_account_v1", "kubernetes_default_service_account", + "kubernetes_config_map_v1", "kubernetes_config_map", "kubernetes_config_map_v1_data", + "kubernetes_secret_v1", "kubernetes_secret", + "kubernetes_pod_v1", "kubernetes_pod", + "kubernetes_endpoints_v1", "kubernetes_endpoints", + "kubernetes_limit_range_v1", "kubernetes_limit_range", + "kubernetes_persistent_volume_v1", "kubernetes_persistent_volume", + "kubernetes_persistent_volume_claim_v1", "kubernetes_persistent_volume_claim", + "kubernetes_replication_controller_v1", "kubernetes_replication_controller", + "kubernetes_resource_quota_v1", "kubernetes_resource_quota", + + // Api registration. + "kubernetes_api_service_v1", "kubernetes_api_service", + + // Apps. + "kubernetes_deployment_v1", "kubernetes_deployment", + "kubernetes_daemon_set_v1", "kubernetes_daemonset", "kubernetes_daemon_set", + "kubernetes_stateful_set_v1", "kubernetes_stateful_set", + + // Batch. + "kubernetes_cron_job_v1", + "kubernetes_cron_job", + "kubernetes_job_v1", "kubernetes_job", + + // Autoscaling. + "kubernetes_horizontal_pod_autoscaler_v2", + "kubernetes_horizontal_pod_autoscaler_v2beta2", + "kubernetes_horizontal_pod_autoscaler_v1", "kubernetes_horizontal_pod_autoscaler", + + // Certificates. + "kubernetes_certificate_signing_request_v1", + "kubernetes_certificate_signing_request", + + // Rbac. + "kubernetes_role_v1", "kubernetes_role", + "kubernetes_role_binding_v1", "kubernetes_role_binding", + "kubernetes_cluster_role_v1", "kubernetes_cluster_role", + "kubernetes_cluster_role_binding_v1", "kubernetes_cluster_role_binding", + + // Networking. + "kubernetes_ingress_v1", + "kubernetes_ingress", + "kubernetes_ingress_class_v1", "kubernetes_ingress_class", + "kubernetes_network_policy_v1", "kubernetes_network_policy", + + // Policy. + "kubernetes_pod_disruption_budget_v1", + "kubernetes_pod_disruption_budget", + "kubernetes_pod_security_policy_v1beta1", "kubernetes_pod_security_policy", + + // Scheduling. + "kubernetes_priority_class_v1", "kubernetes_priority_class", + + // Admission control. + "kubernetes_validating_webhook_configuration_v1", + "kubernetes_validating_webhook_configuration", + "kubernetes_mutating_webhook_configuration_v1", + "kubernetes_mutating_webhook_configuration", + + // Storage. + "kubernetes_storage_class_v1", "kubernetes_storage_class", + "kubernetes_csi_driver_v1", "kubernetes_csi_driver", +} + +var TFLabeledTypes = []string{ + // Core. + "kubernetes_pod_v1", "kubernetes_pod", + "kubernetes_replication_controller_v1", "kubernetes_replication_controller", + "kubernetes_persistent_volume_v1", "kubernetes_persistent_volume", + "kubernetes_persistent_volume_claim_v1", "kubernetes_persistent_volume_claim", + "kubernetes_service", "kubernetes_service_v1", + + // Apps. + "kubernetes_deployment_v1", "kubernetes_deployment", + "kubernetes_daemon_set_v1", "kubernetes_daemonset", "kubernetes_daemon_set", + "kubernetes_stateful_set_v1", "kubernetes_stateful_set", + + // Batch. + "kubernetes_cron_job_v1", + "kubernetes_cron_job", + "kubernetes_job_v1", "kubernetes_job", + + // Networking. + "kubernetes_ingress", "kubernetes_ingress_v1", +} + +var TFEndpointsTypes = []string{ + // Core. + "kubernetes_service", + "kubernetes_service_v1", + + // Networking. + "kubernetes_ingress", + "kubernetes_ingress_v1", + + // Kubectl_manifest resources. + "kubectl_manifest", + + // Helm resources. + "helm_release", + + // Docker containers. + "docker_container", +} diff --git a/pkg/resourcehandlers/k8s/intercept/types.go b/pkg/resourcehandlers/k8s/intercept/types.go new file mode 100644 index 000000000..7d6d91b7c --- /dev/null +++ b/pkg/resourcehandlers/k8s/intercept/types.go @@ -0,0 +1,26 @@ +package intercept + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Converter holds the functions to transfer the given string to a schema descriptor. +type Converter interface { + // GetGVK returns the GroupVersionKind info with the given alias, + // and returns false if failed to convert. + GetGVK(alias string) (gvk schema.GroupVersionKind, ok bool) + + // GetGVR returns the GroupVersionResource info with the given alias, + // and returns false if failed to convert. + GetGVR(alias string) (gvr schema.GroupVersionResource, ok bool) +} + +// Enforcer holds the functions to judge the given schema descriptor, +// whether to be interested in. +type Enforcer interface { + // AllowGVK returns true if the given GroupVersionKind is valid. + AllowGVK(gvk schema.GroupVersionKind) (valid bool) + + // AllowGVR returns true if the given GroupVersionResource is valid. + AllowGVR(gvr schema.GroupVersionResource) (valid bool) +} diff --git a/pkg/resourcehandlers/k8s/key/codec.go b/pkg/resourcehandlers/k8s/key/codec.go new file mode 100644 index 000000000..5c6576495 --- /dev/null +++ b/pkg/resourcehandlers/k8s/key/codec.go @@ -0,0 +1,30 @@ +package key + +import ( + "strings" + + "github.com/seal-io/utils/stringx" +) + +// Decode parses the given string into {pod namespace, pod name, container type, container name}, +// returns false if not a valid key, e.g. default/coredns-64897985d-6x2jm/container/coredns. +// Valid container types have `initContainer`, `ephemeralContainer`, `container`. +func Decode(s string) (podNamespace, podName, containerType, containerName string, ok bool) { + ss := strings.SplitN(s, "/", 4) + + ok = len(ss) == 4 + if !ok { + return + } + podNamespace = ss[0] + podName = ss[1] + containerType = ss[2] + containerName = ss[3] + + return +} + +// Encode constructs the given {pod namespace, pod name, container type, container name} into a valid key. +func Encode(podNamespace, podName, containerType, containerName string) string { + return stringx.Join("/", podNamespace, podName, containerType, containerName) +} diff --git a/pkg/resourcehandlers/k8s/kube/namspaced_name.go b/pkg/resourcehandlers/k8s/kube/namspaced_name.go new file mode 100644 index 000000000..d1131db39 --- /dev/null +++ b/pkg/resourcehandlers/k8s/kube/namspaced_name.go @@ -0,0 +1,27 @@ +package kube + +import ( + "strings" + + "github.com/seal-io/utils/stringx" +) + +// ParseNamespacedName parses the given string into {namespace, name}, +// e.g. kube-system/coredns. +func ParseNamespacedName(s string) (ns, n string) { + ss := strings.SplitN(s, "/", 2) + if len(ss) == 2 { + return ss[0], ss[1] + } + // Use default namespace provided by kubeconfig. + return "", s +} + +// NamespacedName constructs the given {namespace, name} into one string. +func NamespacedName(ns, n string) string { + if ns == "" { + return n + } + + return stringx.Join("/", ns, n) +} diff --git a/pkg/resourcehandlers/k8s/kube/pod.go b/pkg/resourcehandlers/k8s/kube/pod.go new file mode 100644 index 000000000..fef965ee4 --- /dev/null +++ b/pkg/resourcehandlers/k8s/kube/pod.go @@ -0,0 +1,256 @@ +package kube + +import ( + core "k8s.io/api/core/v1" + + "github.com/seal-io/walrus/pkg/resourcehandlers/k8s/key" +) + +// IsPodReady returns true if Pod is ready. +func IsPodReady(pod *core.Pod) bool { + if !IsPodRunning(pod) { + return false + } + + c, exist := GetPodCondition(&pod.Status, core.PodReady) + if exist { + return c.Status == core.ConditionTrue + } + + return false +} + +// IsPodRunning returns ture if Pod is running. +func IsPodRunning(pod *core.Pod) bool { + if !IsPodAssigned(pod) { + return false + } + + return pod.Status.Phase == core.PodRunning +} + +// IsPodAssigned returns true if Pod is assigned. +func IsPodAssigned(pod *core.Pod) bool { + if pod == nil { + return false + } + + return pod.Spec.NodeName != "" +} + +func IsPodSucceeded(pod *core.Pod) bool { + if !IsPodAssigned(pod) { + return false + } + + return pod.Status.Phase == core.PodSucceeded +} + +// GetPodCondition extracts the provided condition from the given PodStatus and returns that. +func GetPodCondition(status *core.PodStatus, conditionType core.PodConditionType) (c *core.PodCondition, exist bool) { + if status == nil { + return + } + + for i := range status.Conditions { + if status.Conditions[i].Type == conditionType { + return &status.Conditions[i], true + } + } + + return +} + +// ContainerType indicates the type of the Container, +// includes Run, Init, Ephemeral. +type ContainerType = string + +const ( + ContainerRun = "run" + ContainerInit = "init" + ContainerEphemeral = "ephemeral" +) + +var ContainerTypeOrderMap = map[ContainerType]int{ + ContainerRun: 0, + ContainerInit: 1, + ContainerEphemeral: 2, +} + +// Container holds container type and name. +type Container struct { + Type ContainerType + Name string +} + +// IsContainerRunning returns true if Container is running. +func IsContainerRunning(pod *core.Pod, c Container) bool { + if pod == nil || c.Name == "" { + return false + } + + css := make([]*[]core.ContainerStatus, 0, 3) + + switch c.Type { + case ContainerRun: + css = append(css, &pod.Status.ContainerStatuses) + case ContainerInit: + css = append(css, &pod.Status.InitContainerStatuses) + case ContainerEphemeral: + css = append(css, &pod.Status.EphemeralContainerStatuses) + default: + css = append(css, + &pod.Status.ContainerStatuses, + &pod.Status.InitContainerStatuses, + &pod.Status.EphemeralContainerStatuses, + ) + } + + for i := 0; i < len(css); i++ { + cs := *css[i] + for j := 0; j < len(cs); j++ { + if cs[j].Name != c.Name { + continue + } + + return cs[j].State.Running != nil + } + } + + return false +} + +// IsContainerExisted returns true if Container is existed. +func IsContainerExisted(pod *core.Pod, c Container) bool { + if pod == nil || c.Name == "" { + return false + } + + switch c.Type { + case ContainerRun: + for i := range pod.Spec.Containers { + if pod.Spec.Containers[i].Name == c.Name { + return true + } + } + case ContainerInit: + for i := range pod.Spec.InitContainers { + if pod.Spec.InitContainers[i].Name == c.Name { + return true + } + } + case ContainerEphemeral: + for i := range pod.Spec.EphemeralContainers { + if pod.Spec.EphemeralContainers[i].Name == c.Name { + return true + } + } + default: + for i := range pod.Spec.Containers { + if pod.Spec.Containers[i].Name == c.Name { + return true + } + } + + for i := range pod.Spec.InitContainers { + if pod.Spec.InitContainers[i].Name == c.Name { + return true + } + } + + for i := range pod.Spec.EphemeralContainers { + if pod.Spec.EphemeralContainers[i].Name == c.Name { + return true + } + } + } + + return false +} + +// ContainerStateType indicates the state type of the Container, +// includes Unknown, Waiting, Running, Terminated. +type ContainerStateType uint8 + +const ( + ContainerStateUnknown ContainerStateType = iota + ContainerStateWaiting + ContainerStateRunning + ContainerStateTerminated +) + +func GetContainerStateType(s *core.ContainerStatus) ContainerStateType { + currentState := s.State + + switch { + case currentState.Waiting != nil: + switch currentState.Waiting.Reason { + case "PodInitializing": + return ContainerStateWaiting + case "CrashLoopBackOff": + // NB(thxCode): Be able to get log of a failed running container. + return ContainerStateTerminated + } + case currentState.Running != nil: + return ContainerStateRunning + case currentState.Terminated != nil: + return ContainerStateTerminated + } + + return ContainerStateUnknown +} + +type ContainerState struct { + Type ContainerType + Namespace string + Pod string + ID string + Name string + State ContainerStateType +} + +func (c ContainerState) String() string { + return key.Encode(c.Namespace, c.Pod, c.Type, c.Name) +} + +// GetContainerStates returns ContainerState list of the given Pod. +func GetContainerStates(pod *core.Pod) (r []ContainerState) { + if pod == nil { + return + } + + css := []struct { + Type ContainerType + Statuses *[]core.ContainerStatus + }{ + { + Type: ContainerInit, + Statuses: &pod.Status.InitContainerStatuses, + }, + { + Type: ContainerRun, + Statuses: &pod.Status.ContainerStatuses, + }, + { + Type: ContainerEphemeral, + Statuses: &pod.Status.EphemeralContainerStatuses, + }, + } + for i := 0; i < len(css); i++ { + cs := *css[i].Statuses + for j := 0; j < len(cs); j++ { + s := &cs[j] + + r = append(r, ContainerState{ + Type: css[i].Type, + Namespace: pod.Namespace, + Pod: pod.Name, + Name: s.Name, + ID: s.ContainerID, + State: GetContainerStateType(s), + }) + } + } + + return r +} diff --git a/pkg/resourcehandlers/k8s/kubelabel/labels.go b/pkg/resourcehandlers/k8s/kubelabel/labels.go new file mode 100644 index 000000000..97c250a4c --- /dev/null +++ b/pkg/resourcehandlers/k8s/kubelabel/labels.go @@ -0,0 +1,150 @@ +package kubelabel + +import ( + "context" + "fmt" + "reflect" + + "github.com/seal-io/utils/json" + "go.uber.org/multierr" + core "k8s.io/api/core/v1" + kmeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + typesk8s "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + + "github.com/seal-io/walrus/pkg/resourcehandlers/k8s/polymorphic" +) + +// Apply applies the labels to kubernetes resource. +func Apply( + ctx context.Context, + dynamicCli *dynamic.DynamicClient, + o *unstructured.Unstructured, + labels map[string]string, +) error { + p := patcher{ + dynamicCli: dynamicCli, + } + + switch o.GetKind() { + case + "Service", + "Ingress", + "Pod", + "PersistentVolume", "PersistentVolumeClaim": + return p.applyLabels(ctx, o, labels) + case + "Deployment", + "DaemonSet", + "StatefulSet", + "Job", + "ReplicationController", "ReplicaSet": + return p.applyLabelsToWorkloads(ctx, o, labels) + } + + return nil +} + +type patcher struct { + dynamicCli *dynamic.DynamicClient +} + +func (p *patcher) applyLabelsToWorkloads( + ctx context.Context, + o *unstructured.Unstructured, + labels map[string]string, +) error { + pods, err := p.selectPods(ctx, o) + if err != nil { + return err + } + + var berr error + + for i := range pods { + err = p.applyLabels(ctx, &pods[i], labels) + multierr.AppendInto(&berr, err) + } + + return berr +} + +func (p *patcher) applyLabels(ctx context.Context, o *unstructured.Unstructured, labels map[string]string) error { + var ( + ns = o.GetNamespace() + name = o.GetName() + gvk = o.GetObjectKind().GroupVersionKind() + gvr, _ = kmeta.UnsafeGuessKindToResource(gvk) + ) + + metadata, err := kmeta.Accessor(o) + if err != nil { + return fmt.Errorf("error get metadata for %s %s/%s: %w", gvr.Resource, ns, name, err) + } + + origin := metadata.GetLabels() + + update := metadata.GetLabels() + if update == nil { + update = make(map[string]string, len(labels)) + } + + for k, v := range labels { + update[k] = v + } + + // Unchanged. + if reflect.DeepEqual(origin, update) { + return nil + } + + // Change. + patchBytes, err := json.Marshal(map[string]any{ + "metadata": map[string]any{ + "labels": update, + }, + }) + if err != nil { + return fmt.Errorf("error create labels patch: %w", err) + } + + _, err = p.dynamicCli.Resource(gvr).Namespace(ns).Patch( + ctx, + name, + typesk8s.StrategicMergePatchType, + patchBytes, + metav1.PatchOptions{}, + ) + if err != nil { + return fmt.Errorf("error patch labels to %s %s/%s: %w", gvr.Resource, ns, name, err) + } + + return nil +} + +func (p *patcher) selectPods(ctx context.Context, o *unstructured.Unstructured) ([]unstructured.Unstructured, error) { + var ( + name = o.GetName() + gvk = o.GetObjectKind().GroupVersionKind() + gvr, _ = kmeta.UnsafeGuessKindToResource(gvk) + ) + + ns, s, err := polymorphic.SelectorsForObject(o) + if err != nil { + return nil, fmt.Errorf("error gettting selector of kubernetes %s %s/%s: %w", gvr.Resource, ns, name, err) + } + + ss := s.String() + + pl, err := p.dynamicCli. + Resource(core.SchemeGroupVersion.WithResource("pods")). + Namespace(ns). + List(ctx, metav1.ListOptions{ResourceVersion: "0", LabelSelector: ss}) + if err != nil { + return nil, fmt.Errorf("error listing kubernetes %s pods with %s: %w", ns, ss, err) + } + + return pl.Items, nil +} diff --git a/pkg/resourcehandlers/k8s/polymorphic/selectors.go b/pkg/resourcehandlers/k8s/polymorphic/selectors.go new file mode 100644 index 000000000..2d5b8a737 --- /dev/null +++ b/pkg/resourcehandlers/k8s/polymorphic/selectors.go @@ -0,0 +1,57 @@ +package polymorphic + +import ( + "fmt" + + "github.com/seal-io/utils/json" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" +) + +// SelectorsForObject returns the Pod label selector for a given object. +func SelectorsForObject(obj *unstructured.Unstructured) (ns string, s labels.Selector, err error) { + switch obj.GetKind() { + default: + return "", nil, fmt.Errorf("selector for %s not implemented", obj.GetKind()) + case "ReplicaSet", + "StatefulSet", "DaemonSet", "Deployment", + "Job": + ns = obj.GetNamespace() + + lso, exist, _ := unstructured.NestedFieldNoCopy(obj.Object, "spec", "selector") + if !exist { + return "", nil, fmt.Errorf("%s defined without a selector", + obj.GetKind()) + } + + // Any -> bs -> structure. + lsob, err := json.Marshal(lso) + if err != nil { + return "", nil, fmt.Errorf("failed %s marshall, %w", + obj.GetKind(), err) + } + + var ls meta.LabelSelector + if err = json.Unmarshal(lsob, &ls); err != nil { + return "", nil, fmt.Errorf("failed %s unmarshall, %w", + obj.GetKind(), err) + } + + s, err = meta.LabelSelectorAsSelector(&ls) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector, %w", err) + } + case "ReplicationController", "Service": + ns = obj.GetNamespace() + + ss, exist, _ := unstructured.NestedStringMap(obj.Object, "spec", "selector") + if !exist { + return "", nil, fmt.Errorf("%s defined without a selector", + obj.GetKind()) + } + s = labels.SelectorFromSet(ss) + } + + return ns, s, nil +} diff --git a/pkg/resourcehandlers/k8s/polymorphic/serializer.go b/pkg/resourcehandlers/k8s/polymorphic/serializer.go new file mode 100644 index 000000000..6d7fb7636 --- /dev/null +++ b/pkg/resourcehandlers/k8s/polymorphic/serializer.go @@ -0,0 +1,43 @@ +package polymorphic + +import ( + "fmt" + + "github.com/seal-io/utils/json" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + serializerjson "k8s.io/apimachinery/pkg/runtime/serializer/json" +) + +type metaFactory struct{} + +func (metaFactory) Interpret(data []byte) (*schema.GroupVersionKind, error) { + var gvk runtime.TypeMeta + if err := json.Unmarshal(data, &gvk); err != nil { + return nil, fmt.Errorf("error unmarsalling runtime type meta: %w", err) + } + + gv, err := schema.ParseGroupVersion(gvk.APIVersion) + if err != nil { + return nil, fmt.Errorf("error pasring runtime group version: %w", err) + } + + return &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: gvk.Kind}, nil +} + +var ( + mf = metaFactory{} + creator = unstructuredscheme.NewUnstructuredCreator() + typer = unstructuredscheme.NewUnstructuredObjectTyper() +) + +func JsonSerializer() runtime.Serializer { + opts := serializerjson.SerializerOptions{Yaml: false, Pretty: false, Strict: false} + return serializerjson.NewSerializerWithOptions(mf, creator, typer, opts) +} + +func YamlSerializer() runtime.Serializer { + opts := serializerjson.SerializerOptions{Yaml: true, Pretty: false, Strict: false} + return serializerjson.NewSerializerWithOptions(mf, creator, typer, opts) +} diff --git a/pkg/resourcehandlers/k8s/resourcehandler.go b/pkg/resourcehandlers/k8s/resourcehandler.go new file mode 100644 index 000000000..8baa94cde --- /dev/null +++ b/pkg/resourcehandlers/k8s/resourcehandler.go @@ -0,0 +1,139 @@ +package k8s + +import ( + "context" + "time" + + "github.com/seal-io/utils/stringx" + "github.com/seal-io/utils/waitx" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + dynamicclient "k8s.io/client-go/dynamic" + batchclient "k8s.io/client-go/kubernetes/typed/batch/v1" + coreclient "k8s.io/client-go/kubernetes/typed/core/v1" + networkingclient "k8s.io/client-go/kubernetes/typed/networking/v1" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + "github.com/seal-io/walrus/pkg/resourcehandler" +) + +const OperatorType = resourcehandler.ConnectorTypeKubernetes + +// New returns resourcehandlers.ResourceHandler with the given options. +func New(ctx context.Context, opts resourcehandler.CreateOptions) (resourcehandler.ResourceHandler, error) { + restConfig, err := GetConfig(ctx, opts, func(c *rest.Config) { + c.Timeout = 0 + }) + if err != nil { + return nil, err + } + + restCli, err := rest.HTTPClientFor(restConfig) + if err != nil { + return nil, err + } + + coreCli, err := coreclient.NewForConfigAndClient(restConfig, restCli) + if err != nil { + return nil, err + } + + batchCli, err := batchclient.NewForConfigAndClient(restConfig, restCli) + if err != nil { + return nil, err + } + + networkingCli, err := networkingclient.NewForConfigAndClient(restConfig, restCli) + if err != nil { + return nil, err + } + + dynamicCli, err := dynamicclient.NewForConfigAndClient(restConfig, restCli) + if err != nil { + return nil, err + } + + op := Operator{ + Logger: klog.Background().WithName("resourcehandlers").WithName("k8s"), + Identifier: stringx.SumBySHA256("k8s:", restConfig.Host, restConfig.APIPath), + RestConfig: restConfig, + CoreCli: coreCli, + BatchCli: batchCli, + NetworkingCli: networkingCli, + DynamicCli: dynamicCli, + } + + return op, nil +} + +type Operator struct { + Logger klog.Logger + Identifier string + RestConfig *rest.Config + CoreCli *coreclient.CoreV1Client + BatchCli *batchclient.BatchV1Client + NetworkingCli *networkingclient.NetworkingV1Client + DynamicCli *dynamicclient.DynamicClient +} + +// Type implements resourcehandlers.ResourceHandler. +func (Operator) Type() resourcehandler.Type { + return OperatorType +} + +// IsConnected implements resourcehandlers.ResourceHandler. +func (op Operator) IsConnected(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + err := waitx.PollUntilContextCancel(ctx, time.Second, true, + func(ctx context.Context) error { + return IsConnected(context.TODO(), op.CoreCli.RESTClient()) + }, + ) + + return err +} + +// Burst implements resourcehandlers.ResourceHandler. +func (op Operator) Burst() int { + if op.RestConfig.Burst == 0 { + return rest.DefaultBurst + } + + return op.RestConfig.Burst +} + +// ID implements resourcehandlers.ResourceHandler. +func (op Operator) ID() string { + return op.Identifier +} + +// GetComponents implements resourcehandlers.ResourceHandler. +func (op Operator) GetComponents( + ctx context.Context, + resource *walrus.ResourceComponents, +) ([]*walrus.ResourceComponents, error) { + return nil, nil +} + +// Log implements resourcehandlers.ResourceHandler. +func (op Operator) Log(ctx context.Context, key string, opts resourcehandler.LogOptions) error { + return nil +} + +// Exec implements resourcehandlers.ResourceHandler. +func (op Operator) Exec(ctx context.Context, key string, opts resourcehandler.ExecOptions) error { + return nil +} + +func (op Operator) GetKeys(ctx context.Context, component *walrus.ResourceComponents) (*resourcehandler.ResourceComponentOperationKeys, error) { + return nil, nil +} + +func (op Operator) GetStatus(ctx context.Context, component *walrus.ResourceComponents) ([]meta.Condition, error) { + // TODO: Implement this method after resource is migrated. + + return nil, nil +} diff --git a/pkg/resourcehandlers/registry.go b/pkg/resourcehandlers/registry.go new file mode 100644 index 000000000..939456fb4 --- /dev/null +++ b/pkg/resourcehandlers/registry.go @@ -0,0 +1,48 @@ +package resourcehandlers + +import ( + "context" + "fmt" + + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/resourcehandlers/alibaba" + "github.com/seal-io/walrus/pkg/resourcehandlers/aws" + "github.com/seal-io/walrus/pkg/resourcehandlers/azure" + "github.com/seal-io/walrus/pkg/resourcehandlers/docker" + "github.com/seal-io/walrus/pkg/resourcehandlers/google" + "github.com/seal-io/walrus/pkg/resourcehandlers/k8s" + "github.com/seal-io/walrus/pkg/resourcehandlers/unknown" +) + +var opOperators map[resourcehandler.Type]resourcehandler.Creator + +func init() { + opOperators = map[resourcehandler.Type]resourcehandler.Creator{ + // Register resourcehandlers creators as below. + k8s.OperatorType: k8s.New, + aws.OperatorType: aws.New, + alibaba.OperatorType: alibaba.New, + azure.OperatorType: azure.New, + google.OperatorType: google.New, + docker.OperatorType: docker.New, + } +} + +// Get returns ResourceHandler with the given CreateOptions. +func Get(ctx context.Context, opts resourcehandler.CreateOptions) (op resourcehandler.ResourceHandler, err error) { + f, exist := opOperators[opts.Connector.Spec.Type] + if !exist { + // Try to create an any resourcehandlers. + op, err = unknown.New(ctx, opts) + if err != nil { + return nil, fmt.Errorf("unknown resourcehandlers: %s", opts.Connector.Spec.Type) + } + } else { + op, err = f(ctx, opts) + if err != nil { + return nil, fmt.Errorf("error connecting %s resourcehandlers: %w", opts.Connector.Spec.Type, err) + } + } + + return op, nil +} diff --git a/pkg/resourcehandlers/types/credentials.go b/pkg/resourcehandlers/types/credentials.go new file mode 100644 index 000000000..04186613c --- /dev/null +++ b/pkg/resourcehandlers/types/credentials.go @@ -0,0 +1,66 @@ +package types + +import ( + "context" + "errors" +) + +type CredentialKeyType string + +const ( + CredentialKey CredentialKeyType = "credential" +) + +const ( + AccessKey = "access_key" + AccessSecret = "secret_key" + Region = "region" +) + +type Credential struct { + AccessKey string + AccessSecret string + Region string +} + +func GetCredential(configData map[string][]byte) (*Credential, error) { + cred := &Credential{} + + cred.AccessKey = string(configData[AccessKey]) + if cred.AccessKey == "" { + return nil, errors.New("accessKey is empty") + } + + cred.AccessSecret = string(configData[AccessSecret]) + if cred.AccessSecret == "" { + return nil, errors.New("accessSecret is empty") + } + + cred.Region = string(configData[Region]) + if cred.Region == "" { + return nil, errors.New("region is empty") + } + + return cred, nil +} + +func CredentialFromCtx(ctx context.Context) (*Credential, error) { + cred, ok := ctx.Value(CredentialKey).(*Credential) + if !ok { + return nil, errors.New("not found credential from context") + } + + if cred.AccessKey == "" { + return nil, errors.New("accessKey is empty") + } + + if cred.AccessSecret == "" { + return nil, errors.New("secretKey is empty") + } + + if cred.Region == "" { + return nil, errors.New("region is empty") + } + + return cred, nil +} diff --git a/pkg/resourcehandlers/types/util.go b/pkg/resourcehandlers/types/util.go new file mode 100644 index 000000000..5bb8c03e0 --- /dev/null +++ b/pkg/resourcehandlers/types/util.go @@ -0,0 +1,31 @@ +package types + +import ( + "context" + "fmt" + + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrlcli "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/system" +) + +func GetConfigData(ctx context.Context, opts resourcehandler.CreateOptions) (map[string][]byte, error) { + con := opts.Connector + + sec := &core.Secret{ + ObjectMeta: meta.ObjectMeta{ + Namespace: con.Namespace, + Name: con.Spec.SecretName, + }, + } + + cli := system.LoopbackCtrlClient.Get() + err := cli.Get(ctx, ctrlcli.ObjectKeyFromObject(sec), sec) + if err != nil { + return nil, fmt.Errorf("error get secret %s: %w", sec.Name, err) + } + return sec.Data, nil +} diff --git a/pkg/resourcehandlers/types/wrap_resource.go b/pkg/resourcehandlers/types/wrap_resource.go new file mode 100644 index 000000000..dffdb8a18 --- /dev/null +++ b/pkg/resourcehandlers/types/wrap_resource.go @@ -0,0 +1,16 @@ +package types + +import ( + "context" + + "github.com/seal-io/walrus/pkg/resourcehandler" +) + +type ExecutableResource interface { + Exec(ctx context.Context, key string, opts resourcehandler.ExecOptions) error + Supported(ctx context.Context, key string) (bool, error) +} + +type LoggableResource interface { + Log(ctx context.Context, key string, opts resourcehandler.LogOptions) error +} diff --git a/pkg/resourcehandlers/unknown/resourcehandler.go b/pkg/resourcehandlers/unknown/resourcehandler.go new file mode 100644 index 000000000..3decfa925 --- /dev/null +++ b/pkg/resourcehandlers/unknown/resourcehandler.go @@ -0,0 +1,66 @@ +package unknown + +import ( + "context" + "errors" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + "github.com/seal-io/walrus/pkg/resourcehandler" +) + +const OperatorType = "Unknown" + +// New returns types.ResourceHandler with the given options. +func New(ctx context.Context, opts resourcehandler.CreateOptions) (resourcehandler.ResourceHandler, error) { + if opts.Connector.Spec.Category != walruscore.ConnectorCategoryCustom { + return nil, errors.New("not custom connector") + } + + return Operator{}, nil +} + +type Operator struct{} + +func (Operator) Type() resourcehandler.Type { + return OperatorType +} + +func (Operator) IsConnected(ctx context.Context) error { + return nil +} + +func (op Operator) Burst() int { + return 100 +} + +func (op Operator) ID() string { + return "" +} + +func (op Operator) GetComponents( + ctx context.Context, + resource *walrus.ResourceComponents, +) ([]*walrus.ResourceComponents, error) { + return nil, nil +} + +func (Operator) Log(ctx context.Context, key string, opts resourcehandler.LogOptions) error { + return nil +} + +func (Operator) Exec(ctx context.Context, key string, opts resourcehandler.ExecOptions) error { + return nil +} + +func (op Operator) GetKeys(ctx context.Context, component *walrus.ResourceComponents) (*resourcehandler.ResourceComponentOperationKeys, error) { + return nil, nil +} + +func (op Operator) GetStatus(ctx context.Context, component *walrus.ResourceComponents) ([]meta.Condition, error) { + // TODO: Implement this method after resource is migrated. + + return nil, nil +} diff --git a/pkg/resourcehandlers/validate.go b/pkg/resourcehandlers/validate.go new file mode 100644 index 000000000..63e5d3264 --- /dev/null +++ b/pkg/resourcehandlers/validate.go @@ -0,0 +1,35 @@ +package resourcehandlers + +import ( + "context" + "errors" + "fmt" + + ctrlcli "sigs.k8s.io/controller-runtime/pkg/client" + + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + "github.com/seal-io/walrus/pkg/resourcehandler" +) + +func IsConnected(ctx context.Context, conn *walruscore.Connector, client ctrlcli.Client) error { + switch conn.Spec.Category { + case walruscore.ConnectorCategoryKubernetes, walruscore.ConnectorCategoryCloudProvider: + op, err := Get(ctx, resourcehandler.CreateOptions{ + Connector: *conn, + }) + if err != nil { + return err + } + + if err = op.IsConnected(ctx); err != nil { + return fmt.Errorf("unreachable connector: %w", err) + } + + case walruscore.ConnectorCategoryCustom: + + default: + return errors.New("invalid connector category") + } + + return nil +} diff --git a/pkg/systemkuberes/connector.go b/pkg/systemkuberes/connector.go new file mode 100644 index 000000000..16c399027 --- /dev/null +++ b/pkg/systemkuberes/connector.go @@ -0,0 +1,148 @@ +package systemkuberes + +import ( + "context" + "fmt" + + dtypes "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + "github.com/seal-io/walrus/pkg/clients/clientset" + "github.com/seal-io/walrus/pkg/kubeclientset" + "github.com/seal-io/walrus/pkg/kubeconfig" + "github.com/seal-io/walrus/pkg/resourcehandler" + "github.com/seal-io/walrus/pkg/system" +) + +func installDefaultKubernetesConnector( + ctx context.Context, + cli clientset.Interface, + project string, + envType walruscore.EnvironmentType, +) (*walrus.Connector, error) { + connCli := cli.WalrusV1().Connectors(project) + + config, err := readKubeConfig() + if err != nil { + return nil, fmt.Errorf("read kube config: %w", err) + } + + c := &walrus.Connector{ + ObjectMeta: meta.ObjectMeta{ + Namespace: project, + Name: DefaultConnectorName, + }, + Spec: walruscore.ConnectorSpec{ + Type: resourcehandler.ConnectorTypeKubernetes, + Description: "Local Kubernetes", + Category: walruscore.ConnectorCategoryKubernetes, + ApplicableEnvironmentType: envType, + Config: walruscore.ConnectorConfig{ + Version: "v1", + Data: map[string]walruscore.ConnectorConfigEntry{ + "kubeconfig": { + Value: config, + Visible: false, + }, + }, + }, + }, + } + + conn, err := kubeclientset.Update(ctx, connCli, c, kubeclientset.WithCreateIfNotExisted[*walrus.Connector]()) + if err != nil { + return nil, fmt.Errorf("install default kubernetes connector: %w", err) + } + + return conn, nil +} + +func readKubeConfig() (string, error) { + kubeConfig := system.LoopbackKubeClientConfig.Get() + + kc, err := kubeconfig.ConvertRestConfigToApiConfig(&kubeConfig) + if err != nil { + return "", err + } + + kcData, err := clientcmd.Write(kc) + if err != nil { + return "", err + } + + return string(kcData), err +} + +func installDefaultDockerConnector( + ctx context.Context, + cli clientset.Interface, + project string, + envType walruscore.EnvironmentType, +) (*walrus.Connector, error) { + connCli := cli.WalrusV1().Connectors(project) + + c := &walrus.Connector{ + ObjectMeta: meta.ObjectMeta{ + Namespace: project, + Name: DefaultConnectorName, + }, + Spec: walruscore.ConnectorSpec{ + Type: resourcehandler.ConnectorTypeDocker, + Category: walruscore.ConnectorCategoryDocker, + ApplicableEnvironmentType: envType, + Config: walruscore.ConnectorConfig{ + Version: "v1", + Data: map[string]walruscore.ConnectorConfigEntry{}, + }, + }, + } + + conn, err := kubeclientset.Create(ctx, connCli, c) + if err != nil { + return nil, fmt.Errorf("install default docker connector: %w", err) + } + + if err := applyLocalDockerNetwork(ctx); err != nil { + return nil, fmt.Errorf("apply local docker network: %w", err) + } + + return conn, nil +} + +func applyLocalDockerNetwork(ctx context.Context) error { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return err + } + + networkName := "local-walrus" + + networks, err := cli.NetworkList(ctx, dtypes.NetworkListOptions{}) + if err != nil { + return err + } + + exists := false + + for _, n := range networks { + if n.Name == networkName { + exists = true + break + } + } + + if !exists { + _, err = cli.NetworkCreate(ctx, networkName, dtypes.NetworkCreate{ + Driver: "bridge", + }) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/systemkuberes/environment.go b/pkg/systemkuberes/environment.go index 20500d6b5..c86f90def 100644 --- a/pkg/systemkuberes/environment.go +++ b/pkg/systemkuberes/environment.go @@ -7,18 +7,34 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" "github.com/seal-io/walrus/pkg/clients/clientset" "github.com/seal-io/walrus/pkg/kubeclientset" "github.com/seal-io/walrus/pkg/kubeclientset/review" "github.com/seal-io/walrus/pkg/system" + "github.com/seal-io/walrus/pkg/systemsetting" ) -// DefaultEnvironmentName is the Kubernetes Namespace name for the default environment. -const DefaultEnvironmentName = DefaultProjectName + "-local" +const ( + // DefaultEnvironmentName is the Kubernetes Namespace name for the default environment. + DefaultEnvironmentName = DefaultProjectName + "-local" + + // DefaultConnectorName is the name of the default connector. + DefaultConnectorName = "local" +) // InstallDefaultEnvironment creates the default environment, alias to Kubernetes Namespace default-local. func InstallDefaultEnvironment(ctx context.Context, cli clientset.Interface) error { - err := review.CanDoCreate(ctx, + localEnvironmentMode, err := systemsetting.DefaultEnvironmentMode.Value(ctx) + if err != nil { + return err + } + + if localEnvironmentMode == "disabled" { + return nil + } + + err = review.CanDoCreate(ctx, cli.AuthorizationV1().SelfSubjectAccessReviews(), review.Simples{ { @@ -32,6 +48,13 @@ func InstallDefaultEnvironment(ctx context.Context, cli clientset.Interface) err return err } + environmentType := func() walruscore.EnvironmentType { + if system.LoopbackKubeInside.Get() { + return walruscore.EnvironmentTypeProduction + } + return walruscore.EnvironmentTypeDevelopment + }() + envCli := cli.WalrusV1().Environments(DefaultProjectName) env := &walrus.Environment{ ObjectMeta: meta.ObjectMeta{ @@ -39,12 +62,7 @@ func InstallDefaultEnvironment(ctx context.Context, cli clientset.Interface) err Name: DefaultEnvironmentName, }, Spec: walrus.EnvironmentSpec{ - Type: func() walrus.EnvironmentType { - if system.LoopbackKubeInside.Get() { - return walrus.EnvironmentTypeProduction - } - return walrus.EnvironmentTypeDevelopment - }(), + Type: environmentType, DisplayName: "Default Environment", Description: "The default environment created by Walrus.", }, @@ -55,5 +73,42 @@ func InstallDefaultEnvironment(ctx context.Context, cli clientset.Interface) err return fmt.Errorf("install default environment: %w", err) } + // Install default connector. + var conn *walrus.Connector + switch localEnvironmentMode { + case "kubernetes": + conn, err = installDefaultKubernetesConnector(ctx, cli, DefaultProjectName, environmentType) + if err != nil { + return err + } + case "docker": + conn, err = installDefaultDockerConnector(ctx, cli, DefaultProjectName, environmentType) + if err != nil { + return err + } + default: + return fmt.Errorf("invalid local environment mode %q", localEnvironmentMode) + } + + // Create connector binding. + bindingsCli := cli.WalrusV1().ConnectorBindings(DefaultEnvironmentName) + connectorBinding := &walrus.ConnectorBinding{ + ObjectMeta: meta.ObjectMeta{ + Namespace: DefaultEnvironmentName, + Name: DefaultConnectorName, + }, + Spec: walruscore.ConnectorBindingSpec{ + Connector: walruscore.ConnectorReference{ + Name: conn.Name, + Namespace: conn.Namespace, + }, + }, + } + + _, err = kubeclientset.Create(ctx, bindingsCli, connectorBinding) + if err != nil { + return fmt.Errorf("install default connector binding: %w", err) + } + return nil } diff --git a/pkg/webhooks/setup.go b/pkg/webhooks/setup.go index 0a5647bc9..9c5bcbf9a 100644 --- a/pkg/webhooks/setup.go +++ b/pkg/webhooks/setup.go @@ -24,6 +24,7 @@ var ( setupers = []webhook.Setup{ new(walruscore.CatalogWebhook), new(walruscore.ConnectorWebhook), + new(walruscore.ConnectorBindingWebhook), new(walruscore.ResourceDefinitionWebhook), new(walruscore.TemplateWebhook), } diff --git a/pkg/webhooks/walruscore/connector.go b/pkg/webhooks/walruscore/connector.go index 9d761236c..678a62e0e 100644 --- a/pkg/webhooks/walruscore/connector.go +++ b/pkg/webhooks/walruscore/connector.go @@ -2,42 +2,157 @@ package walruscore import ( "context" + "fmt" + "reflect" + "github.com/seal-io/utils/stringx" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrlcli "sigs.k8s.io/controller-runtime/pkg/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ctrlwebhook "sigs.k8s.io/controller-runtime/pkg/webhook" ctrladmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + "github.com/seal-io/walrus/pkg/kubeclientset" + "github.com/seal-io/walrus/pkg/kubemeta" + "github.com/seal-io/walrus/pkg/resourcehandlers" "github.com/seal-io/walrus/pkg/webhook" ) // ConnectorWebhook hooks a v1.Connector object. // // nolint: lll -// +k8s:webhook-gen:validating:group="walruscore.seal.io",version="v1",resource="connectors",scope="Namespaced" +// +k8s:webhook-gen:mutating:group="walruscore.seal.io",version="v1",resource="connectors",scope="Namespaced" +// +k8s:webhook-gen:mutating:operations=["CREATE","UPDATE"],failurePolicy="Fail",sideEffects="NoneOnDryRun",matchPolicy="Equivalent",timeoutSeconds=10 +// +k8s:webhook-gen:validating:group="walruscore.seal.io",version="v1",resource="connectors",scope="Namespaced",subResources=["status"] // +k8s:webhook-gen:validating:operations=["CREATE","UPDATE","DELETE"],failurePolicy="Fail",sideEffects="None",matchPolicy="Equivalent",timeoutSeconds=10 -type ConnectorWebhook struct{} +type ConnectorWebhook struct { + client ctrlcli.Client +} func (r *ConnectorWebhook) SetupWebhook(_ context.Context, opts webhook.SetupOptions) (runtime.Object, error) { + r.client = opts.Manager.GetClient() + return &walruscore.Connector{}, nil } var _ ctrlwebhook.CustomValidator = (*ConnectorWebhook)(nil) func (r *ConnectorWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (ctrladmission.Warnings, error) { - // TODO: your logic here + logger := ctrllog.FromContext(ctx) + + conn := obj.(*walruscore.Connector) + if stringx.StringWidth(conn.Name) > 30 { + err := r.deleteSecret(ctx, conn) + if err != nil { + logger.Error(err, "failed to delete secret") + } + + return nil, field.TooLongMaxLength(field.NewPath("name"), stringx.StringWidth(conn.Name), 30) + } + + err := resourcehandlers.IsConnected(ctx, conn, r.client) + if err != nil { + derr := r.deleteSecret(ctx, conn) + if derr != nil { + logger.Error(derr, "failed to delete secret") + } + + return nil, err + } return nil, nil } func (r *ConnectorWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (ctrladmission.Warnings, error) { - // TODO: your logic here + conn := newObj.(*walruscore.Connector) + if !reflect.DeepEqual(oldObj.(*walruscore.Connector).Spec, conn.Spec) { + err := resourcehandlers.IsConnected(ctx, conn, r.client) + if err != nil { + return nil, err + } + } return nil, nil } func (r *ConnectorWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (ctrladmission.Warnings, error) { - // TODO: your logic here + conn := obj.(*walruscore.Connector) + labelSelector := labels.SelectorFromSet(labels.Set{ + "walrus.seal.io/connector": fmt.Sprintf("%s-%s", conn.Namespace, conn.Name), + }) + + cbList := new(walruscore.ConnectorBindingList) + err := r.client.List(ctx, cbList, ctrlcli.MatchingLabelsSelector{Selector: labelSelector}) + if err != nil { + return nil, err + } + + if len(cbList.Items) > 0 { + return nil, field.Forbidden(field.NewPath("metadata", "name"), fmt.Sprintf("used by environment %s", cbList.Items[0].Namespace)) + } return nil, nil } + +func (r *ConnectorWebhook) Default(ctx context.Context, obj runtime.Object) error { + conn := obj.(*walruscore.Connector) + + if conn.DeletionTimestamp != nil { + return nil + } + + data := map[string][]byte{} + for k, v := range conn.Spec.Config.Data { + data[k] = []byte(v.Value) + } + + name := fmt.Sprintf("connector-config-%s", stringx.SumByFNV64a(conn.Namespace, conn.Name)) + eSec := &core.Secret{ + ObjectMeta: meta.ObjectMeta{ + Namespace: conn.Namespace, + Name: name, + }, + Data: data, + } + + _, err := kubeclientset.UpdateWithCtrlClient(ctx, r.client, eSec, kubeclientset.WithCreateIfNotExisted[*core.Secret]()) + if err != nil { + return err + } + + configData := map[string]walruscore.ConnectorConfigEntry{} + for k, v := range conn.Spec.Config.Data { + if v.Visible { + configData[k] = v + } else { + configData[k] = walruscore.ConnectorConfigEntry{ + Value: "", + Visible: false, + } + } + } + + conn.Spec.Config.Data = configData + conn.Spec.SecretName = name + + kubemeta.SanitizeLastAppliedAnnotation(conn) + + return nil +} + +func (r *ConnectorWebhook) deleteSecret(ctx context.Context, conn *walruscore.Connector) error { + name := conn.Spec.SecretName + eSec := &core.Secret{ + ObjectMeta: meta.ObjectMeta{ + Namespace: conn.Namespace, + Name: name, + }, + } + + return kubeclientset.DeleteWithCtrlClient(ctx, r.client, eSec) +} diff --git a/pkg/webhooks/walruscore/connector_binding.go b/pkg/webhooks/walruscore/connector_binding.go new file mode 100644 index 000000000..cc534d4f6 --- /dev/null +++ b/pkg/webhooks/walruscore/connector_binding.go @@ -0,0 +1,144 @@ +package walruscore + +import ( + "context" + "fmt" + "reflect" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrlcli "sigs.k8s.io/controller-runtime/pkg/client" + ctrlwebhook "sigs.k8s.io/controller-runtime/pkg/webhook" + ctrladmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + "github.com/seal-io/walrus/pkg/webhook" +) + +// ConnectorBindingWebhook hooks a v1.ConnectorBinding object. +// +// nolint: lll +// +k8s:webhook-gen:mutating:group="walruscore.seal.io",version="v1",resource="connectorbindings",scope="Namespaced" +// +k8s:webhook-gen:mutating:operations=["CREATE","UPDATE"],failurePolicy="Fail",sideEffects="NoneOnDryRun",matchPolicy="Equivalent",timeoutSeconds=10 +// +k8s:webhook-gen:validating:group="walruscore.seal.io",version="v1",resource="connectorbindings",scope="Namespaced" +// +k8s:webhook-gen:validating:operations=["CREATE","UPDATE","DELETE"],failurePolicy="Fail",sideEffects="None",matchPolicy="Equivalent",timeoutSeconds=10 +type ConnectorBindingWebhook struct { + client ctrlcli.Client +} + +func (r *ConnectorBindingWebhook) SetupWebhook(_ context.Context, opts webhook.SetupOptions) (runtime.Object, error) { + r.client = opts.Manager.GetClient() + + return &walruscore.ConnectorBinding{}, nil +} + +var _ ctrlwebhook.CustomValidator = (*ConnectorBindingWebhook)(nil) + +func (r *ConnectorBindingWebhook) ValidateCreate( + ctx context.Context, + obj runtime.Object, +) (ctrladmission.Warnings, error) { + cb := obj.(*walruscore.ConnectorBinding) + + conn := &walruscore.Connector{ + ObjectMeta: meta.ObjectMeta{ + Name: cb.Spec.Connector.Name, + Namespace: cb.Spec.Connector.Namespace, + }, + } + err := r.client.Get(ctx, ctrlcli.ObjectKeyFromObject(conn), conn) + if err != nil { + return nil, err + } + + // Validate connector applicable environment type. + { + envList := new(walrus.EnvironmentList) + err = r.client.List(ctx, envList, ctrlcli.MatchingFields{"metadata.name": cb.Namespace}) + if err != nil { + return nil, err + } + + if len(envList.Items) != 1 { + return nil, field.Forbidden( + field.NewPath("metadata", "namespace"), "connector must be created to a valid environment") + } + + if envList.Items[0].Spec.Type != conn.Spec.ApplicableEnvironmentType { + return nil, field.Invalid( + field.NewPath("spec", "connector"), + cb.Spec.Connector, + fmt.Sprintf("connector must be created to a %s environment", conn.Spec.ApplicableEnvironmentType), + ) + } + } + + // Validate duplicated binding. + { + cbList := new(walruscore.ConnectorBindingList) + err := r.client.List(ctx, cbList, ctrlcli.InNamespace(cb.Namespace)) + if err != nil { + return nil, err + } + + for i := range cbList.Items { + if cbList.Items[i].Status.Category == conn.Spec.Category && + cbList.Items[i].Status.Type == conn.Spec.Type { + return nil, field.Invalid( + field.NewPath("spec", "connector"), + cb.Spec.Connector, + "connectors for the same purpose cannot be bound repeatedly", + ) + } + } + } + + return nil, nil +} + +func (r *ConnectorBindingWebhook) ValidateUpdate( + ctx context.Context, + oldObj, newObj runtime.Object, +) (ctrladmission.Warnings, error) { + oldCb, newCb := oldObj.(*walruscore.ConnectorBinding), newObj.(*walruscore.ConnectorBinding) + if !reflect.DeepEqual(oldCb.Spec, newCb.Spec) { + return nil, field.Forbidden(field.NewPath("spec"), "cannot update connector binding spec") + } + + return nil, nil +} + +func (r *ConnectorBindingWebhook) ValidateDelete( + ctx context.Context, + obj runtime.Object, +) (ctrladmission.Warnings, error) { + return nil, nil +} + +func (r *ConnectorBindingWebhook) Default(ctx context.Context, obj runtime.Object) error { + cb := obj.(*walruscore.ConnectorBinding) + if cb.DeletionTimestamp != nil { + return nil + } + + connector := &walruscore.Connector{ + ObjectMeta: meta.ObjectMeta{ + Name: cb.Spec.Connector.Name, + Namespace: cb.Spec.Connector.Namespace, + }, + } + if err := r.client.Get(ctx, ctrlcli.ObjectKeyFromObject(connector), connector); err != nil { + return err + } + + labels := cb.Labels + if labels == nil { + labels = map[string]string{} + } + labels["walrus.seal.io/connector"] = fmt.Sprintf("%s-%s", cb.Spec.Connector.Namespace, cb.Spec.Connector.Name) + cb.SetLabels(labels) + + return nil +}