diff --git a/Dockerfile b/Dockerfile index 1ba5bb6e..8d02a485 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,7 @@ RUN go mod download COPY cmd/main.go cmd/main.go COPY api/ api/ COPY internal/ internal/ +COPY providers/ providers/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command @@ -43,6 +44,7 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags FROM gcr.io/distroless/static:nonroot WORKDIR / COPY --from=builder /workspace/manager . +COPY --from=builder /workspace/providers /opt/providers/ USER 65532:65532 ENTRYPOINT ["/manager"] diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index a8e1b31d..75e8b720 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -42,10 +42,8 @@ type ( ) const ( - // Provider CAPA + // Provider AWS ProviderAWSName = "cluster-api-provider-aws" - // Provider Azure - ProviderAzureName = "cluster-api-provider-azure" // Provider vSphere ProviderVSphereName = "cluster-api-provider-vsphere" // Provider OpenStack diff --git a/api/v1alpha1/management_types.go b/api/v1alpha1/management_types.go index 645bf3a6..9abe83b1 100644 --- a/api/v1alpha1/management_types.go +++ b/api/v1alpha1/management_types.go @@ -90,6 +90,10 @@ type Provider struct { Name string `json:"name"` } +func (p Provider) String() string { + return p.Name +} + func (in *Component) HelmValues() (values map[string]any, err error) { if in.Config != nil { err = yaml.Unmarshal(in.Config.Raw, &values) @@ -97,17 +101,6 @@ func (in *Component) HelmValues() (values map[string]any, err error) { return values, err } -func GetDefaultProviders() []Provider { - return []Provider{ - {Name: ProviderK0smotronName}, - {Name: ProviderAWSName}, - {Name: ProviderAzureName}, - {Name: ProviderVSphereName}, - {Name: ProviderOpenStackName}, - {Name: ProviderSveltosName}, - } -} - // Templates returns a list of provider templates explicitly defined in the Management object func (in *Management) Templates() []string { templates := []string{} diff --git a/cmd/main.go b/cmd/main.go index 03ee5f50..2411ce79 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -15,9 +15,12 @@ package main import ( + "cmp" "crypto/tls" "flag" + "fmt" "os" + "strings" hcv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" @@ -27,7 +30,6 @@ import ( "k8s.io/client-go/dynamic" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth" - capz "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" capv "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -36,8 +38,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" hmcmirantiscomv1alpha1 "github.com/Mirantis/hmc/api/v1alpha1" + "github.com/Mirantis/hmc/internal/build" "github.com/Mirantis/hmc/internal/controller" "github.com/Mirantis/hmc/internal/helm" + "github.com/Mirantis/hmc/internal/providers" "github.com/Mirantis/hmc/internal/telemetry" "github.com/Mirantis/hmc/internal/utils" hmcwebhook "github.com/Mirantis/hmc/internal/webhook" @@ -55,12 +59,16 @@ func init() { utilruntime.Must(sourcev1.AddToScheme(scheme)) utilruntime.Must(hcv2.AddToScheme(scheme)) utilruntime.Must(sveltosv1beta1.AddToScheme(scheme)) - utilruntime.Must(capz.AddToScheme(scheme)) utilruntime.Must(capv.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } func main() { + providersGlob := cmp.Or(os.Getenv("PROVIDERS_PATH_GLOB"), "/opt/providers/*.yml") + if err := providers.RegisterProvidersFromGlob(providersGlob); err != nil { + panic(fmt.Sprintf("failed to register providers: %v", err)) + } + var ( metricsAddr string probeAddr string @@ -107,6 +115,24 @@ func main() { Development: true, } opts.BindFlags(flag.CommandLine) + + flag.Usage = func() { + var defaultUsage strings.Builder + { + oldOutput := flag.CommandLine.Output() + flag.CommandLine.SetOutput(&defaultUsage) + flag.PrintDefaults() + flag.CommandLine.SetOutput(oldOutput) + } + + _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + _, _ = fmt.Fprint(os.Stderr, defaultUsage.String()) + _, _ = fmt.Fprintf(os.Stderr, "\nSupported providers:\n") + for _, el := range providers.List() { + _, _ = fmt.Fprintf(os.Stderr, " - %s\n", el) + } + _, _ = fmt.Fprintf(os.Stderr, "\nVersion: %s\n", build.Version) + } flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) diff --git a/go.mod b/go.mod index a087df64..f9a88bc3 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/fluxcd/pkg/apis/meta v1.8.0 github.com/fluxcd/pkg/runtime v0.50.1 github.com/fluxcd/source-controller/api v1.4.1 + github.com/go-logr/logr v1.4.2 github.com/google/uuid v1.6.0 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/onsi/ginkgo/v2 v2.22.1 @@ -20,6 +21,7 @@ require ( github.com/segmentio/analytics-go v3.1.0+incompatible github.com/stretchr/testify v1.10.0 github.com/vmware-tanzu/velero v1.15.0 + github.com/yuin/gopher-lua v1.1.1 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.16.4 k8s.io/api v0.31.3 @@ -29,7 +31,6 @@ require ( k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 sigs.k8s.io/cluster-api v1.9.3 sigs.k8s.io/cluster-api-operator v0.14.0 - sigs.k8s.io/cluster-api-provider-azure v1.17.2 sigs.k8s.io/cluster-api-provider-vsphere v1.12.0 sigs.k8s.io/controller-runtime v0.19.3 sigs.k8s.io/yaml v1.4.0 @@ -38,9 +39,6 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/BurntSushi/toml v1.4.0 // indirect @@ -84,7 +82,6 @@ require ( github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-ldap/ldap/v3 v3.4.8 // indirect - github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -162,17 +159,17 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect - github.com/yuin/gopher-lua v1.1.1 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.46.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect go.opentelemetry.io/otel/trace v1.30.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 // indirect - golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.10.0 // indirect diff --git a/go.sum b/go.sum index 92fa1ef3..a2a84dca 100644 --- a/go.sum +++ b/go.sum @@ -4,27 +4,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I= -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/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/azure-service-operator/v2 v2.8.0 h1:BcyB8LvRmtgVIIUaXwWIJz5eHvknyno0qq5LkDuvM/s= -github.com/Azure/azure-service-operator/v2 v2.8.0/go.mod h1:ezbJS56PcORFFqLV8XZmM9xZ12m6aGAkg353fQhWD/8= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 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.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -55,8 +38,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= -github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -200,8 +181,6 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -257,7 +236,7 @@ github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFO github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= @@ -374,8 +353,6 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -682,8 +659,6 @@ sigs.k8s.io/cluster-api v1.9.3 h1:lKWbrXzyNmJh++IcX54ZbAmnO7tZ2wKgds7WvskpiXY= sigs.k8s.io/cluster-api v1.9.3/go.mod h1:5iojv38PSvOd4cxqu08Un5TQmy2yBkd3+0U7R/e+msk= sigs.k8s.io/cluster-api-operator v0.14.0 h1:0QgO6+XGrNNJnNHKBwvQD5v6w+EaH3Z0RL1nL3wpjA4= sigs.k8s.io/cluster-api-operator v0.14.0/go.mod h1:euShpVN6HyxXas28HkrYxhCPVDW1UV6ljbRBAeCxp8Y= -sigs.k8s.io/cluster-api-provider-azure v1.17.2 h1:uS9ggE/bryI0hiOWHBa56nYHkWmsPZW3bzYeAddL4vM= -sigs.k8s.io/cluster-api-provider-azure v1.17.2/go.mod h1:ohdf0TYutOn5vKsXpNVeZUVfUSNIwNhfF6wDjbiqPI0= sigs.k8s.io/cluster-api-provider-vsphere v1.12.0 h1:9ze+1JSdLAGiLklsnORvj/vs2XpR9jyVmkT0Dwo1nuc= sigs.k8s.io/cluster-api-provider-vsphere v1.12.0/go.mod h1:2y9fsZQ3qjT1kL6IXiOUVcyV0n8DLBQGvyPnId9xRzk= sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw= diff --git a/internal/controller/clusterdeployment_controller.go b/internal/controller/clusterdeployment_controller.go index 4d2c3f93..88ef12e6 100644 --- a/internal/controller/clusterdeployment_controller.go +++ b/internal/controller/clusterdeployment_controller.go @@ -50,6 +50,7 @@ import ( hmc "github.com/Mirantis/hmc/api/v1alpha1" "github.com/Mirantis/hmc/internal/credspropagation" "github.com/Mirantis/hmc/internal/helm" + providersloader "github.com/Mirantis/hmc/internal/providers" "github.com/Mirantis/hmc/internal/sveltos" "github.com/Mirantis/hmc/internal/telemetry" "github.com/Mirantis/hmc/internal/utils/status" @@ -575,35 +576,16 @@ func (r *ClusterDeploymentReconciler) releaseCluster(ctx context.Context, namesp return err } - var ( - gvkAWSCluster = schema.GroupVersionKind{ - Group: "infrastructure.cluster.x-k8s.io", - Version: "v1beta2", - Kind: "AWSCluster", - } - - gvkAzureCluster = schema.GroupVersionKind{ - Group: "infrastructure.cluster.x-k8s.io", - Version: "v1beta1", - Kind: "AzureCluster", - } - - gvkMachine = schema.GroupVersionKind{ - Group: "cluster.x-k8s.io", - Version: "v1beta1", - Kind: "Machine", - } - ) - - providerGVKs := map[string]schema.GroupVersionKind{ - "aws": gvkAWSCluster, - "azure": gvkAzureCluster, + gvkMachine := schema.GroupVersionKind{ + Group: "cluster.x-k8s.io", + Version: "v1beta1", + Kind: "Machine", } // Associate the provider with it's GVK for _, provider := range providers { - gvk, ok := providerGVKs[provider] - if !ok { + gvk := providersloader.GetClusterGVK(provider) + if !gvk.Empty() { continue } @@ -637,13 +619,12 @@ func (r *ClusterDeploymentReconciler) getInfraProvidersNames(ctx context.Context return nil, err } - const infraPrefix = "infrastructure-" var ( ips = make([]string, 0, len(template.Status.Providers)) - lprefix = len(infraPrefix) + lprefix = len(providersloader.InfraPrefix) ) for _, v := range template.Status.Providers { - if idx := strings.Index(v, infraPrefix); idx > -1 { + if idx := strings.Index(v, providersloader.InfraPrefix); idx > -1 { ips = append(ips, v[idx+lprefix:]) } } @@ -719,54 +700,36 @@ func (r *ClusterDeploymentReconciler) reconcileCredentialPropagation(ctx context } for _, provider := range providers { - switch provider { - case "aws": - l.Info("Skipping creds propagation for AWS") - case "azure": - l.Info("Azure creds propagation start") - if err := credspropagation.PropagateAzureSecrets(ctx, propnCfg); err != nil { - errMsg := fmt.Sprintf("failed to create Azure CCM credentials: %s", err) - apimeta.SetStatusCondition(clusterDeployment.GetConditions(), metav1.Condition{ - Type: hmc.CredentialsPropagatedCondition, - Status: metav1.ConditionFalse, - Reason: hmc.FailedReason, - Message: errMsg, - }) - - return errors.New(errMsg) - } + titleName := providersloader.GetProviderTitleName(provider) + f, ok := providersloader.CredentialPropagationFunc(provider) + if !ok || titleName == "" { apimeta.SetStatusCondition(clusterDeployment.GetConditions(), metav1.Condition{ Type: hmc.CredentialsPropagatedCondition, - Status: metav1.ConditionTrue, - Reason: hmc.SucceededReason, - Message: "Azure CCM credentials created", + Status: metav1.ConditionFalse, + Reason: hmc.FailedReason, + Message: "unsupported infrastructure provider " + provider, }) - case "vsphere": - l.Info("vSphere creds propagation start") - if err := credspropagation.PropagateVSphereSecrets(ctx, propnCfg); err != nil { - errMsg := fmt.Sprintf("failed to create vSphere CCM credentials: %s", err) - apimeta.SetStatusCondition(clusterDeployment.GetConditions(), metav1.Condition{ - Type: hmc.CredentialsPropagatedCondition, - Status: metav1.ConditionFalse, - Reason: hmc.FailedReason, - Message: errMsg, - }) - return errors.New(errMsg) - } + continue + } + + enabled, err := f(ctx, propnCfg, l) + if err != nil { + errMsg := fmt.Sprintf("failed to create %s CCM credentials: %s", titleName, err) apimeta.SetStatusCondition(clusterDeployment.GetConditions(), metav1.Condition{ Type: hmc.CredentialsPropagatedCondition, - Status: metav1.ConditionTrue, - Reason: hmc.SucceededReason, - Message: "vSphere CCM credentials created", + Status: metav1.ConditionFalse, + Reason: hmc.FailedReason, + Message: errMsg, }) - default: + return errors.New(errMsg) + } else if enabled { apimeta.SetStatusCondition(clusterDeployment.GetConditions(), metav1.Condition{ Type: hmc.CredentialsPropagatedCondition, - Status: metav1.ConditionFalse, - Reason: hmc.FailedReason, - Message: "unsupported infrastructure provider " + provider, + Status: metav1.ConditionTrue, + Reason: hmc.SucceededReason, + Message: titleName + " CCM credentials created", }) } } diff --git a/internal/controller/release_controller.go b/internal/controller/release_controller.go index be19aa87..5fcb4ade 100644 --- a/internal/controller/release_controller.go +++ b/internal/controller/release_controller.go @@ -47,6 +47,7 @@ import ( hmc "github.com/Mirantis/hmc/api/v1alpha1" "github.com/Mirantis/hmc/internal/build" "github.com/Mirantis/hmc/internal/helm" + "github.com/Mirantis/hmc/internal/providers" "github.com/Mirantis/hmc/internal/utils" ) @@ -194,7 +195,7 @@ func (r *ReleaseReconciler) ensureManagement(ctx context.Context) error { if err != nil { return err } - mgmtObj.Spec.Providers = hmc.GetDefaultProviders() + mgmtObj.Spec.Providers = providers.List() getter := helm.NewMemoryRESTClientGetter(r.Config, r.RESTMapper()) actionConfig := new(action.Configuration) diff --git a/internal/credspropagation/azure.go b/internal/credspropagation/azure.go deleted file mode 100644 index 53abb495..00000000 --- a/internal/credspropagation/azure.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2024 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package credspropagation - -import ( - "context" - "encoding/json" - "fmt" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - capz "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func PropagateAzureSecrets(ctx context.Context, cfg *PropagationCfg) error { - azureCluster := &capz.AzureCluster{} - if err := cfg.Client.Get(ctx, client.ObjectKey{ - Name: cfg.ClusterDeployment.Name, - Namespace: cfg.ClusterDeployment.Namespace, - }, azureCluster); err != nil { - return fmt.Errorf("failed to get AzureCluster %s: %w", cfg.ClusterDeployment.Name, err) - } - - azureClIdty := &capz.AzureClusterIdentity{} - if err := cfg.Client.Get(ctx, client.ObjectKey{ - Name: azureCluster.Spec.IdentityRef.Name, - Namespace: azureCluster.Spec.IdentityRef.Namespace, - }, azureClIdty); err != nil { - return fmt.Errorf("failed to get AzureClusterIdentity %s: %w", azureCluster.Spec.IdentityRef.Name, err) - } - - azureSecret := &corev1.Secret{} - if err := cfg.Client.Get(ctx, client.ObjectKey{ - Name: azureClIdty.Spec.ClientSecret.Name, - Namespace: azureClIdty.Spec.ClientSecret.Namespace, - }, azureSecret); err != nil { - return fmt.Errorf("failed to get azure Secret %s: %w", azureClIdty.Spec.ClientSecret.Name, err) - } - - ccmSecret, err := generateAzureCCMSecret(azureCluster, azureClIdty, azureSecret) - if err != nil { - return fmt.Errorf("failed to generate Azure CCM secret: %w", err) - } - - if err := applyCCMConfigs(ctx, cfg.KubeconfSecret, ccmSecret); err != nil { - return fmt.Errorf("failed to apply Azure CCM secret: %w", err) - } - - return nil -} - -func generateAzureCCMSecret(azureCluster *capz.AzureCluster, azureClIdty *capz.AzureClusterIdentity, azureSecret *corev1.Secret) (*corev1.Secret, error) { - subnetName, secGroup, routeTable := getAzureSubnetData(azureCluster) - azureJSONMap := map[string]any{ - "cloud": azureCluster.Spec.AzureEnvironment, - "tenantId": azureClIdty.Spec.TenantID, - "subscriptionId": azureCluster.Spec.SubscriptionID, - "aadClientId": azureClIdty.Spec.ClientID, - "aadClientSecret": string(azureSecret.Data["clientSecret"]), - "resourceGroup": azureCluster.Spec.ResourceGroup, - "securityGroupName": secGroup, - "securityGroupResourceGroup": azureCluster.Spec.NetworkSpec.Vnet.ResourceGroup, - "location": azureCluster.Spec.Location, - "vmType": "vmss", - "vnetName": azureCluster.Spec.NetworkSpec.Vnet.Name, - "vnetResourceGroup": azureCluster.Spec.NetworkSpec.Vnet.ResourceGroup, - "subnetName": subnetName, - "routeTableName": routeTable, - "loadBalancerSku": "Standard", - "loadBalancerName": "", - "maximumLoadBalancerRuleCount": 250, - "useManagedIdentityExtension": false, - "useInstanceMetadata": true, - } - azureJSON, err := json.Marshal(azureJSONMap) - if err != nil { - return nil, fmt.Errorf("error marshalling azure.json: %w", err) - } - - secretData := map[string][]byte{ - "cloud-config": azureJSON, - } - - return makeSecret("azure-cloud-provider", metav1.NamespaceSystem, secretData), nil -} - -func getAzureSubnetData(azureCluster *capz.AzureCluster) (subnetName, secGroup, routeTable string) { - for _, sn := range azureCluster.Spec.NetworkSpec.Subnets { - if sn.Role == "node" { - subnetName = sn.Name - secGroup = sn.SecurityGroup.Name - routeTable = sn.RouteTable.Name - break - } - } - return subnetName, secGroup, routeTable -} diff --git a/internal/credspropagation/lua.go b/internal/credspropagation/lua.go new file mode 100644 index 00000000..95746f56 --- /dev/null +++ b/internal/credspropagation/lua.go @@ -0,0 +1,367 @@ +// Copyright 2024 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package credspropagation + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + lua "github.com/yuin/gopher-lua" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func convertAnyToLuaValue(l *lua.LState, v any) lua.LValue { + if l == nil { + return lua.LNil + } + + switch val := v.(type) { + case string: + return lua.LString(val) + case []byte: + return lua.LString(string(val)) + case json.RawMessage: + return lua.LString(string(val)) + case int: + return lua.LNumber(val) + case uint: + return lua.LNumber(val) + case float32: + return lua.LNumber(val) + case float64: + return lua.LNumber(val) + case bool: + return lua.LBool(val) + case []any: + arr := l.NewTable() + + for i, item := range val { + arr.RawSetInt(i+1, convertAnyToLuaValue(l, item)) + } + + return arr + case map[string]any: + return convertMapToLuaTable(l, val) + case map[int]any: + table := l.NewTable() + + for k, v := range val { + table.RawSetInt(k, convertAnyToLuaValue(l, v)) + } + + return table + case nil: + return lua.LNil + default: + return lua.LNil + } +} + +func convertMapToLuaTable(l *lua.LState, m map[string]any) *lua.LTable { + if l == nil || m == nil { + return nil + } + + table := l.NewTable() + + for k, v := range m { + table.RawSetString(k, convertAnyToLuaValue(l, v)) + } + + return table +} + +func convertLuaValueToAny(value lua.LValue) any { + if value == nil { + return nil + } + + switch v := value.(type) { + case lua.LString: + return string(v) + case lua.LNumber: + return float64(v) + case lua.LBool: + return bool(v) + case *lua.LTable: + if v == nil { + return nil + } + + if v.MaxN() > 0 { + arr := make([]any, 0, v.MaxN()) + + v.ForEach(func(_, item lua.LValue) { + arr = append(arr, convertLuaValueToAny(item)) + }) + + return arr + } + + return convertLuaTableToMap(v) + default: + return nil + } +} + +func convertLuaTableToMap(table *lua.LTable) map[string]any { + if table == nil { + return nil + } + + result := make(map[string]any) + + table.ForEach(func(key, value lua.LValue) { + switch v := value.(type) { + case lua.LString: + result[key.String()] = string(v) + case lua.LNumber: + result[key.String()] = float64(v) + case lua.LBool: + result[key.String()] = bool(v) + case *lua.LTable: + if v == nil { + return + } + + if v.MaxN() > 0 { + arr := make([]any, 0, v.MaxN()) + + v.ForEach(func(_, item lua.LValue) { + arr = append(arr, convertLuaValueToAny(item)) + }) + + result[key.String()] = arr + } else { + result[key.String()] = convertLuaTableToMap(v) + } + default: + result[key.String()] = nil + } + }) + + return result +} + +func getGVKFromTable(table *lua.LTable) schema.GroupVersionKind { + if table == nil { + return schema.GroupVersionKind{} + } + + group := table.RawGetString("group") + version := table.RawGetString("version") + kind := table.RawGetString("kind") + + if group == lua.LNil || version == lua.LNil || kind == lua.LNil { + return schema.GroupVersionKind{} + } + + return schema.GroupVersionKind{ + Group: group.String(), + Version: version.String(), + Kind: kind.String(), + } +} + +func getObject(ctx context.Context, kubeClient client.Client, l *lua.LState) int { + if l == nil { + l.Push(lua.LNil) + l.Push(lua.LString("undefined state")) + return 2 + } + + if l.GetTop() != 3 { + l.Push(lua.LNil) + l.Push(lua.LString("invalid number of arguments")) + return 2 + } + + gvkTable := l.CheckTable(1) + if gvkTable == nil { + l.Push(lua.LNil) + l.Push(lua.LString("invalid GVK table")) + return 2 + } + + objectName, objectNamespace := l.CheckString(2), l.CheckString(3) + if objectName == "" { + l.Push(lua.LNil) + l.Push(lua.LString("empty object Name")) + return 2 + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(getGVKFromTable(gvkTable)) + + if err := kubeClient.Get(ctx, client.ObjectKey{ + Name: objectName, + Namespace: objectNamespace, + }, obj); err != nil { + l.Push(lua.LNil) + l.Push(lua.LString(fmt.Sprintf("failed to get object %s/%s of kind %s: %v", + objectNamespace, objectName, obj.GetObjectKind().GroupVersionKind().Kind, err))) + return 2 + } + + l.Push(convertMapToLuaTable(l, obj.UnstructuredContent())) + return 1 +} + +func jsonEncode(l *lua.LState) int { + if l == nil { + l.Push(lua.LNil) + l.Push(lua.LString("undefined state")) + return 2 + } + + var val any + + switch v := l.CheckAny(1).(type) { + case *lua.LTable: + if v == nil { + l.Push(lua.LNil) + l.Push(lua.LString("nil table provided for JSON encoding")) + return 2 + } + + val = convertLuaTableToMap(v) + default: + l.Push(lua.LNil) + l.Push(lua.LString(fmt.Sprintf("unsupported type for JSON encoding: %T", v))) + return 2 + } + + b, err := json.Marshal(val) + if err != nil { + l.Push(lua.LNil) + l.Push(lua.LString(err.Error())) + return 2 + } + + l.Push(lua.LString(string(b))) + return 1 +} + +func base64Encode(l *lua.LState) int { + if l == nil { + l.Push(lua.LNil) + l.Push(lua.LString("undefined state")) + return 2 + } + + str := l.CheckString(1) + if str == "" { + l.Push(lua.LNil) + l.Push(lua.LString("empty string provided for base64 encoding")) + return 2 + } + + encoded := base64.StdEncoding.EncodeToString([]byte(str)) + l.Push(lua.LString(encoded)) + return 1 +} + +func getProviderObjects(ctx context.Context, kubeClient client.Client, namespace, name, luaCode string) ([]client.Object, error) { + if namespace == "" || name == "" { + return nil, errors.New("namespace and name must not be empty") + } + + if kubeClient == nil { + return nil, errors.New("kubeClient must not be nil") + } + + l := lua.NewState() + defer l.Close() + + l.SetGlobal("getObject", l.NewFunction(func(l *lua.LState) int { + return getObject(ctx, kubeClient, l) + })) + + l.SetGlobal("jsonEncode", l.NewFunction(jsonEncode)) + l.SetGlobal("base64Encode", l.NewFunction(base64Encode)) + + if err := l.DoString(luaCode); err != nil { + return nil, err + } + + if err := l.CallByParam(lua.P{ + Fn: l.GetGlobal("getObjects"), + NRet: 1, + Protect: true, + }, lua.LString(namespace), lua.LString(name)); err != nil { + return nil, err + } + + result := l.Get(-1) + l.Pop(1) + + if result == lua.LNil { + return nil, nil + } + + resultTable, ok := result.(*lua.LTable) + if !ok { + return nil, errors.New("result is not Lua Table") + } + + var objects []client.Object + + resultTable.ForEach(func(_, value lua.LValue) { + objTable, ok := value.(*lua.LTable) + if !ok { + return + } + + u := &unstructured.Unstructured{ + Object: convertLuaTableToMap(objTable), + } + + if u.GetName() == "" { + return // skip objects without a name + } + + if u.GetKind() == "" { + return // skip objects without a kind + } + + objects = append(objects, u) + }) + + return objects, nil +} + +func PropagateProviderObjects(ctx context.Context, cfg *PropagationCfg, luaCode string) error { + if cfg == nil || luaCode == "" { + return nil + } + + objects, err := getProviderObjects(ctx, cfg.Client, + cfg.ClusterDeployment.Namespace, cfg.ClusterDeployment.Name, luaCode, + ) + if err != nil { + return fmt.Errorf("failed to get Azure CCM objects: %w", err) + } + + if err := applyCCMConfigs(ctx, cfg.KubeconfSecret, objects...); err != nil { + return fmt.Errorf("failed to apply Azure CCM objects: %w", err) + } + + return nil +} diff --git a/internal/credspropagation/vsphere.go b/internal/credspropagation/vsphere.go index f9f3e5ab..7057faba 100644 --- a/internal/credspropagation/vsphere.go +++ b/internal/credspropagation/vsphere.go @@ -30,7 +30,7 @@ import ( hmc "github.com/Mirantis/hmc/api/v1alpha1" ) -func PropagateVSphereSecrets(ctx context.Context, cfg *PropagationCfg) error { +func PropagateVSphereProviderObjects(ctx context.Context, cfg *PropagationCfg) error { vsphereCluster := &capv.VSphereCluster{} if err := cfg.Client.Get(ctx, client.ObjectKey{ Name: cfg.ClusterDeployment.Name, diff --git a/internal/providers/aws.go b/internal/providers/aws.go new file mode 100644 index 00000000..1cddc114 --- /dev/null +++ b/internal/providers/aws.go @@ -0,0 +1,67 @@ +// Copyright 2024 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package providers + +import ( + "context" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/Mirantis/hmc/internal/credspropagation" +) + +type ProviderAWS struct{} + +var _ ProviderModule = (*ProviderAWS)(nil) + +func init() { + Register(&ProviderAWS{}) +} + +func (*ProviderAWS) GetName() string { + return "aws" +} + +func (*ProviderAWS) GetTitleName() string { + return "AWS" +} + +func (*ProviderAWS) GetClusterGVK() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: "infrastructure.cluster.x-k8s.io", + Version: "v1beta2", + Kind: "AWSCluster", + } +} + +func (*ProviderAWS) GetClusterIdentityKinds() []string { + return []string{"AWSClusterStaticIdentity", "AWSClusterRoleIdentity", "AWSClusterControllerIdentity"} +} + +func (p *ProviderAWS) CredentialPropagationFunc() func( + _ context.Context, + _ *credspropagation.PropagationCfg, + l logr.Logger, +) (enabled bool, err error) { + return func( + _ context.Context, + _ *credspropagation.PropagationCfg, + l logr.Logger, + ) (enabled bool, err error) { + l.Info("Skipping creds propagation for " + p.GetTitleName()) + return enabled, err + } +} diff --git a/internal/providers/providers.go b/internal/providers/providers.go new file mode 100644 index 00000000..0b1a8573 --- /dev/null +++ b/internal/providers/providers.go @@ -0,0 +1,159 @@ +// Copyright 2024 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package providers + +import ( + "context" + "fmt" + "slices" + "strings" + "sync" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + + hmc "github.com/Mirantis/hmc/api/v1alpha1" + "github.com/Mirantis/hmc/internal/credspropagation" +) + +const ( + // InfraPrefix is the prefix used for infrastructure provider names + InfraPrefix = "infrastructure-" + // ProviderPrefix is the prefix used for cluster API provider names + ProviderPrefix = "cluster-api-provider-" +) + +var ( + mu sync.RWMutex + + providers = []hmc.Provider{ + { + Name: hmc.ProviderK0smotronName, + }, + { + Name: hmc.ProviderSveltosName, + }, + } + + registry map[string]ProviderModule +) + +type ProviderModule interface { + // GetName returns the short name of the provider + GetName() string + // GetTitleName returns the display title of the provider + GetTitleName() string + // GetClusterGVK returns the GroupVersionKind for the provider's cluster resource + GetClusterGVK() schema.GroupVersionKind + // GetClusterIdentityKinds returns a list of supported cluster identity kinds + GetClusterIdentityKinds() []string + // CredentialPropagationFunc returns a function to handle credential propagation + CredentialPropagationFunc() func( + ctx context.Context, + cfg *credspropagation.PropagationCfg, + l logr.Logger, + ) (enabled bool, err error) +} + +// Register adds a new provider module to the registry +func Register(p ProviderModule) { + mu.Lock() + defer mu.Unlock() + + if registry == nil { + registry = make(map[string]ProviderModule) + } + + shortName := p.GetName() + + if _, exists := registry[shortName]; exists { + panic(fmt.Sprintf("provider %q already registered", shortName)) + } + + providers = append(providers, + hmc.Provider{ + Name: ProviderPrefix + p.GetName(), + }, + ) + + registry[shortName] = p +} + +// List returns a copy of all registered providers +func List() []hmc.Provider { + return slices.Clone(providers) +} + +// CredentialPropagationFunc returns the credential propagation function for a given provider +func CredentialPropagationFunc(fullName string) ( + func(ctx context.Context, propnCfg *credspropagation.PropagationCfg, l logr.Logger) (enabled bool, err error), bool, +) { + mu.RLock() + defer mu.RUnlock() + + shortName := strings.TrimPrefix(fullName, ProviderPrefix) + + module, ok := registry[shortName] + if !ok { + return nil, false + } + + f := module.CredentialPropagationFunc() + + return f, f != nil +} + +// GetClusterGVK returns the GroupVersionKind for a provider's cluster resource +func GetClusterGVK(shortName string) schema.GroupVersionKind { + mu.RLock() + defer mu.RUnlock() + + module, ok := registry[shortName] + if !ok { + return schema.GroupVersionKind{} + } + + return module.GetClusterGVK() +} + +// GetClusterIdentityKind returns the supported identity kinds for a given infrastructure provider +func GetClusterIdentityKind(infraName string) ([]string, bool) { + mu.RLock() + defer mu.RUnlock() + + shortName := strings.TrimPrefix(infraName, InfraPrefix) + + module, ok := registry[shortName] + if !ok { + return nil, false + } + + list := slices.Clone(module.GetClusterIdentityKinds()) + + return list, list != nil +} + +// GetProviderTitleName returns the display title for a given provider +func GetProviderTitleName(shortName string) string { + mu.RLock() + defer mu.RUnlock() + + module, ok := registry[shortName] + if !ok { + return "" + } + + return module.GetTitleName() +} diff --git a/internal/providers/vsphere.go b/internal/providers/vsphere.go new file mode 100644 index 00000000..e2d1396d --- /dev/null +++ b/internal/providers/vsphere.go @@ -0,0 +1,64 @@ +// Copyright 2024 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package providers + +import ( + "context" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/Mirantis/hmc/internal/credspropagation" +) + +type ProvidervSphere struct{} + +var _ ProviderModule = (*ProvidervSphere)(nil) + +func init() { + Register(&ProvidervSphere{}) +} + +func (*ProvidervSphere) GetName() string { + return "vsphere" +} + +func (*ProvidervSphere) GetTitleName() string { + return "vSphere" +} + +func (*ProvidervSphere) GetClusterGVK() schema.GroupVersionKind { + return schema.GroupVersionKind{} +} + +func (*ProvidervSphere) GetClusterIdentityKinds() []string { + return []string{"VSphereClusterIdentity"} +} + +func (p *ProvidervSphere) CredentialPropagationFunc() func( + ctx context.Context, + cfg *credspropagation.PropagationCfg, + l logr.Logger, +) (enabled bool, err error) { + return func( + ctx context.Context, + cfg *credspropagation.PropagationCfg, + l logr.Logger, + ) (enabled bool, err error) { + l.Info(p.GetTitleName() + " creds propagation start") + enabled, err = true, credspropagation.PropagateVSphereProviderObjects(ctx, cfg) + return enabled, err + } +} diff --git a/internal/providers/yaml.go b/internal/providers/yaml.go new file mode 100644 index 00000000..9cfb2b80 --- /dev/null +++ b/internal/providers/yaml.go @@ -0,0 +1,123 @@ +// Copyright 2024 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package providers + +import ( + "context" + "fmt" + "os" + "path/filepath" + "slices" + + "github.com/go-logr/logr" + "gopkg.in/yaml.v3" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/Mirantis/hmc/internal/credspropagation" +) + +// GVK represents the GroupVersionKind structure in YAML +type GVK struct { + Group string `yaml:"group"` + Version string `yaml:"version"` + Kind string `yaml:"kind"` +} + +// CredentialPropagation represents the credential propagation configuration +type CredentialPropagation struct { + Lua string `yaml:"lua"` + Enabled bool `yaml:"enabled"` +} + +// YAMLProviderDefinition represents a YAML-based provider configuration +type YAMLProviderDefinition struct { + Name string `yaml:"name"` + Title string `yaml:"title"` + ClusterGVK GVK `yaml:"clusterGVK"` + ClusterIdentityKinds []string `yaml:"clusterIdentityKinds"` + CredentialPropagation CredentialPropagation `yaml:"credentialPropagation"` +} + +var _ ProviderModule = (*YAMLProviderDefinition)(nil) + +func (p *YAMLProviderDefinition) GetName() string { + return p.Name +} + +func (p *YAMLProviderDefinition) GetTitleName() string { + return p.Title +} + +func (p *YAMLProviderDefinition) GetClusterGVK() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: p.ClusterGVK.Group, + Version: p.ClusterGVK.Version, + Kind: p.ClusterGVK.Kind, + } +} + +func (p *YAMLProviderDefinition) GetClusterIdentityKinds() []string { + return slices.Clone(p.ClusterIdentityKinds) +} + +func (p *YAMLProviderDefinition) CredentialPropagationFunc() func( + ctx context.Context, + cfg *credspropagation.PropagationCfg, + l logr.Logger, +) (enabled bool, err error) { + return func( + ctx context.Context, + cfg *credspropagation.PropagationCfg, + l logr.Logger, + ) (bool, error) { + l.Info(p.GetTitleName() + " creds propagation start") + return p.CredentialPropagation.Enabled, + credspropagation.PropagateProviderObjects(ctx, cfg, p.CredentialPropagation.Lua) + } +} + +// RegisterFromYAML registers a provider from a YAML file +func RegisterFromYAML(yamlFile string) error { + data, err := os.ReadFile(yamlFile) + if err != nil { + return fmt.Errorf("failed to read YAML file: %w", err) + } + + var ypd YAMLProviderDefinition + + if err := yaml.Unmarshal(data, &ypd); err != nil { + return fmt.Errorf("failed to unmarshal YAML: %w", err) + } + + Register(&ypd) + + return nil +} + +// RegisterProvidersFromGlob loads and registers provider YAML files matching the glob pattern +func RegisterProvidersFromGlob(pattern string) error { + matches, err := filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("failed to glob pattern %q: %w", pattern, err) + } + + for _, file := range matches { + if err := RegisterFromYAML(file); err != nil { + return fmt.Errorf("provider %s: %w", filepath.Base(file), err) + } + } + + return nil +} diff --git a/internal/webhook/clusterdeployment_webhook.go b/internal/webhook/clusterdeployment_webhook.go index ef8923c7..f8e998e2 100644 --- a/internal/webhook/clusterdeployment_webhook.go +++ b/internal/webhook/clusterdeployment_webhook.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" hmcv1alpha1 "github.com/Mirantis/hmc/api/v1alpha1" + providersloader "github.com/Mirantis/hmc/internal/providers" ) type ClusterDeploymentValidator struct { @@ -269,29 +270,25 @@ func isCredMatchTemplate(cred *hmcv1alpha1.Credential, template *hmcv1alpha1.Clu } for _, provider := range template.Status.Providers { - switch provider { - case "infrastructure-aws": - if idtyKind != "AWSClusterStaticIdentity" && - idtyKind != "AWSClusterRoleIdentity" && - idtyKind != "AWSClusterControllerIdentity" { - return errMsg(provider) - } - case "infrastructure-azure": - if idtyKind != "AzureClusterIdentity" { - return errMsg(provider) - } - case "infrastructure-vsphere": - if idtyKind != "VSphereClusterIdentity" { - return errMsg(provider) - } - case "infrastructure-internal": + if provider == providersloader.InfraPrefix+"internal" { if idtyKind != "Secret" { return errMsg(provider) } - default: - if strings.HasPrefix(provider, "infrastructure-") { + + continue + } + + idtys, found := providersloader.GetClusterIdentityKind(provider) + if !found { + if strings.HasPrefix(provider, providersloader.InfraPrefix) { return fmt.Errorf("unsupported infrastructure provider %s", provider) } + + continue + } + + if !slices.Contains(idtys, idtyKind) { + return errMsg(provider) } } diff --git a/internal/webhook/clusterdeployment_webhook_test.go b/internal/webhook/clusterdeployment_webhook_test.go index 993f3539..1d039e9a 100644 --- a/internal/webhook/clusterdeployment_webhook_test.go +++ b/internal/webhook/clusterdeployment_webhook_test.go @@ -276,10 +276,18 @@ func TestClusterDeploymentValidateCreate(t *testing.T) { clusterdeployment.WithCredential(testCredentialName), ), existingObjects: []runtime.Object{ - cred, + credential.NewCredential( + credential.WithName(testCredentialName), + credential.WithReady(true), + credential.WithIdentityRef( + &corev1.ObjectReference{ + Kind: "SomeOtherDummyClusterStaticIdentity", + Name: "otherdummyclid", + }), + ), management.NewManagement( management.WithAvailableProviders(v1alpha1.Providers{ - "infrastructure-azure", + "infrastructure-aws", "control-plane-k0smotron", "bootstrap-k0smotron", }), @@ -287,14 +295,14 @@ func TestClusterDeploymentValidateCreate(t *testing.T) { template.NewClusterTemplate( template.WithName(testTemplateName), template.WithProvidersStatus( - "infrastructure-azure", + "infrastructure-aws", "control-plane-k0smotron", "bootstrap-k0smotron", ), template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), ), }, - err: "the ClusterDeployment is invalid: wrong kind of the ClusterIdentity \"AWSClusterStaticIdentity\" for provider \"infrastructure-azure\"", + err: "the ClusterDeployment is invalid: wrong kind of the ClusterIdentity \"SomeOtherDummyClusterStaticIdentity\" for provider \"infrastructure-aws\"", }, } for _, tt := range tests { diff --git a/providers/azure.yml b/providers/azure.yml new file mode 100644 index 00000000..b3f5485b --- /dev/null +++ b/providers/azure.yml @@ -0,0 +1,170 @@ +# Copyright 2024 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: azure +title: Azure +clusterGVK: + group: infrastructure.cluster.x-k8s.io + version: v1beta1 + kind: AzureCluster +clusterIdentityKinds: + - AzureClusterIdentity +credentialPropagation: + enabled: true + lua: | + -- Define GVK configurations + local GVK = { + azureCluster = { + group = "infrastructure.cluster.x-k8s.io", + version = "v1beta1", + kind = "AzureCluster" + }, + azureClusterIdentity = { + group = "infrastructure.cluster.x-k8s.io", + version = "v1beta1", + kind = "AzureClusterIdentity" + }, + secret = { + group = "", + version = "v1", + kind = "Secret" + }, + configMap = { + group = "", + version = "v1", + kind = "ConfigMap" + } + } + + function getObjects(namespace, name) + -- Input validation + if not namespace or namespace == "" then + return nil, "namespace is required" + end + if not name or name == "" then + return nil, "name is required" + end + + -- Get cluster info + local cluster = getObject(GVK.azureCluster, name, namespace) + if cluster == nil then + return nil, "failed to get cluster" + end + + -- Validate cluster spec + if type(cluster.spec) ~= "table" then + return nil, "invalid cluster specification" + end + if not cluster.spec.networkSpec or not cluster.spec.networkSpec.subnets then + return nil, "invalid network specification" + end + if #cluster.spec.networkSpec.subnets == 0 then + return nil, "no subnets configured" + end + + -- Find node subnet + local subnetName, securityGroupName, routeTableName + for _, subnet in ipairs(cluster.spec.networkSpec.subnets) do + if subnet.role == "node" then + subnetName = subnet.name + securityGroupName = subnet.securityGroup.name + routeTableName = subnet.routeTable.name + break + end + end + + if not subnetName then + return nil, "no node subnet found" + end + + -- Get cluster identity + local identity = getObject(GVK.azureClusterIdentity, + cluster.spec.identityRef.name, + cluster.spec.identityRef.namespace) + if identity == nil then + return nil, "failed to get cluster identity" + end + + -- Get client secret + local secret = getObject(GVK.secret, + identity.spec.clientSecret.name, + identity.spec.clientSecret.namespace) + if secret == nil then + return nil, "failed to get client secret" + end + + -- Build config table + local config = { + cloud = cluster.spec.azureEnvironment, + tenantId = identity.spec.tenantID, + subscriptionId = cluster.spec.subscriptionID, + aadClientId = identity.spec.clientID, + aadClientSecret = secret.data.clientSecret, + resourceGroup = cluster.spec.resourceGroup, + securityGroupName = securityGroupName, + securityGroupResourceGroup = cluster.spec.networkSpec.vnet.resourceGroup, + location = cluster.spec.location, + vmType = "vmss", + vnetName = cluster.spec.networkSpec.vnet.name, + vnetResourceGroup = cluster.spec.networkSpec.vnet.resourceGroup, + subnetName = subnetName, + routeTableName = routeTableName, + loadBalancerSku = "Standard", + loadBalancerName = "", + maximumLoadBalancerRuleCount = 250, + useManagedIdentityExtension = false, + useInstanceMetadata = true + } + + -- Validate required fields + local required_fields = {"tenantId", "subscriptionId", "aadClientId", "aadClientSecret", + "resourceGroup", "location"} + for _, field in ipairs(required_fields) do + if not config[field] or config[field] == "" then + return nil, string.format("required field '%s' is missing or empty", field) + end + end + + -- Create cloud config JSON and base64 encode it + local cloudConfig = jsonEncode(config) + if cloudConfig == nil then + return nil, "failed to encode config as JSON" + end + + local base64Config = base64Encode(cloudConfig) + if base64Config == nil then + return nil, "failed to base64 encode cloud config" + end + + local obj = { + apiVersion = GVK.secret.version, + kind = GVK.secret.kind, + metadata = { + name = "azure-cloud-provider", + namespace = "kube-system", + annotations = { + ["kubernetes.io/created-by"] = "credentials-propagation-provider-azure" + } + }, + data = { + ["cloud-config"] = base64Config + }, + type = "Opaque" + } + + -- Return list of objects + local objects = {} + table.insert(objects, obj) + return objects + end