From 5bb044e97c9fa2d169f930e25155ce3cc7fcc441 Mon Sep 17 00:00:00 2001 From: Martin Proffitt Date: Sun, 12 Nov 2023 16:18:45 +0100 Subject: [PATCH] Unit testing --- README.md | 84 +++- awsapi.go | 321 ++----------- awsfunctions.go | 391 ++++++++++++++++ fn.go | 39 +- fn_test.go | 429 +++++++++++++++++- helpers.go | 68 --- launchtemplates.go | 128 ------ .../definition_xrobjectdefinitions.yaml | 85 ++++ pkg/composite/generate.go | 13 + pkg/composite/v1beta1/definition.go | 100 ++++ .../v1beta1/zz_generated.deepcopy.go | 135 ++++++ {input => pkg/input}/generate.go | 2 +- {input => pkg/input}/v1beta1/input.go | 0 .../input}/v1beta1/zz_generated.deepcopy.go | 0 types.go | 30 +- 15 files changed, 1271 insertions(+), 554 deletions(-) create mode 100644 awsfunctions.go delete mode 100644 helpers.go delete mode 100644 launchtemplates.go create mode 100644 package/composite/definition_xrobjectdefinitions.yaml create mode 100644 pkg/composite/generate.go create mode 100644 pkg/composite/v1beta1/definition.go create mode 100644 pkg/composite/v1beta1/zz_generated.deepcopy.go rename {input => pkg/input}/generate.go (91%) rename {input => pkg/input}/v1beta1/input.go (100%) rename {input => pkg/input}/v1beta1/zz_generated.deepcopy.go (100%) diff --git a/README.md b/README.md index f820e7d..054e9e8 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,69 @@ # function-describe-nodegroups A [Crossplane] Composition Function which reads EKS nodegroup information and -uses that to create `AWSManagedMachinePool` objects +uses that to create `MachinePool` anbd `AWSManagedMachinePool` objects. -## How it works +In order to use this function as part of the [Composition], the composition +must be written to use pipeline mode. See the documentation on +[Composition functions] to better understand how this is integrated -> **Warning** -> This plugin is requires Crossplane v1.14 which is currently unreleased -> (due 1st November 2023). -> -> The example composition is also written for Crossplane v1.14 and will -> not work on any current MC version. -> -> To support this, the script [`kind.sh`](./kind.sh) is provided to -> help you understand how this works by spinning crossplane up inside a -> kind cluster for local development. +## Composition integration -In order to use this function as part of the [Composition], the composition -must be written to use pipeline mode. This is a (currently undocumented) -mode for compositions. +This function is placed in the pipeline with a reference to the cluster object +for that composition. + +This should be specified in your composition as follows, setting `clusterRef` +as appropriate for your composition. ```yaml -spec: - compositeTypeRef: - apiVersion: crossplane.giantswarm.io/v1alpha1 - kind: CompositeEksImport - mode: Pipeline - pipeline: - - step: collect-cluster - ... - - step: generate-subnets - ... + - step: describe-nodegroups + functionRef: + name: function-describe-nodegroups + input: + apiVersion: describenodegroups.fn.giantswarm.io + kind: Nodegroups + metadata: + namespace: crossplane + spec: + clusterRef: eks-cluster ``` +## How it works + +### AWS provider + +This function performs a lookup against the AWS API for Nodegroups linked to +the clustername referenced in `clusterRef`. It then iterates over that +information and populates additional information from secondary lookups to the +EC2 API and Autoscaling API as appropriate. + +Once all information is retrieved, this is then populated into a cluster-api +provider-aws `AwsManagedMachinePool` object, wrapped inside a +`crossplane-contrib/provider-kubernetes` object and returned as a desired +resource to be reconciled and created by crossplane. + +If the nodegroup was created with a `LaunchTemplate` then the function will +attempt to use that to build the specification for Cluster API. If no launch +template is found then the function tries to provide all required information +to Cluster Api so that it can formulate the nodepool(s). + +When a launch template is discovered, certain pathways for ClusterAPI are +closed off. + +- It is not possible to change the instance size when using launch templates + +### Azure provider + +TBD + +### GCP Provider + +TBD + +## Known Issues + +[Crossplane]: https://crossplane.io +[crossplane-cli]: https://github.com/crossplane/crossplane/releases/tag/v1.14.0-rc.1 +[Composition]: https://docs.crossplane.io/v1.13/concepts/compositions +[Composition functions]: https://docs.crossplane.io/latest/concepts/compositions/#use-composition-functions +[RunFunctionRequest]: https://github.com/crossplane/function-sdk-go/blob/a4ada4f934f6f8d3f9018581199c6c71e0343d13/proto/v1beta1/run_function.proto#L36 \ No newline at end of file diff --git a/awsapi.go b/awsapi.go index 7c821b1..a89190c 100644 --- a/awsapi.go +++ b/awsapi.go @@ -2,28 +2,38 @@ package main import ( "context" - "fmt" - "strings" "github.com/aws/aws-sdk-go-v2/aws" asg "github.com/aws/aws-sdk-go-v2/service/autoscaling" - asgtypes "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/crossplane/crossplane-runtime/pkg/errors" xfnaws "github.com/giantswarm/xfnlib/pkg/auth/aws" - "github.com/giantswarm/xfnlib/pkg/composite" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - expinfrav2 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" - capiinfra "sigs.k8s.io/cluster-api/api/v1beta1" ) +// EC2API Describes the functions required to access data on the AWS EC2 api +type AwsEc2Api interface { + DescribeLaunchTemplates(ctx context.Context, + params *ec2.DescribeLaunchTemplatesInput, + optFns ...func(*ec2.Options)) (*ec2.DescribeLaunchTemplatesOutput, error) + + DescribeLaunchTemplateVersions(ctx context.Context, + params *ec2.DescribeLaunchTemplateVersionsInput, + optFns ...func(*ec2.Options)) (*ec2.DescribeLaunchTemplateVersionsOutput, error) +} + +// DescribeLaunchTemplateVersions Get the EC2 Launch template versions for a given launch template +func DescribeLaunchTemplateVersions(c context.Context, api AwsEc2Api, input *ec2.DescribeLaunchTemplateVersionsInput) (*ec2.DescribeLaunchTemplateVersionsOutput, error) { + return api.DescribeLaunchTemplateVersions(c, input) +} + +// DescribeLaunchTemplates Find launch templates for a given nodegroup +func DescribeLaunchTemplates(c context.Context, api AwsEc2Api, input *ec2.DescribeLaunchTemplatesInput) (*ec2.DescribeLaunchTemplatesOutput, error) { + return api.DescribeLaunchTemplates(c, input) +} + // EKSNodegroupAPI describes the AWS functions required by this composition function // in order to track nodegroup objects for the desired cluster -type EKSNodegroupsAPI interface { +type AwsEksApi interface { ListNodegroups(ctx context.Context, params *eks.ListNodegroupsInput, optFns ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) @@ -33,296 +43,43 @@ type EKSNodegroupsAPI interface { optFns ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) } -// Get the nodegroups attached to the provided cluster -func GetNodegroups(c context.Context, api EKSNodegroupsAPI, input *eks.ListNodegroupsInput) (*eks.ListNodegroupsOutput, error) { +// GetNodegroups Get the nodegroups attached to the provided cluster +func GetNodegroups(c context.Context, api AwsEksApi, input *eks.ListNodegroupsInput) (*eks.ListNodegroupsOutput, error) { return api.ListNodegroups(c, input) } -// Describe a single nodegroup -func DescribeNodegroup(c context.Context, api EKSNodegroupsAPI, input *eks.DescribeNodegroupInput) (*eks.DescribeNodegroupOutput, error) { +// DescribeNodegroup Describe a single nodegroup +func DescribeNodegroup(c context.Context, api AwsEksApi, input *eks.DescribeNodegroupInput) (*eks.DescribeNodegroupOutput, error) { return api.DescribeNodegroup(c, input) } // AutoscalingAPI presents functions required for reading autoscaling groups from AWS -type AutoscalingAPI interface { +type AwsAsgApi interface { DescribeAutoScalingGroups(ctx context.Context, params *asg.DescribeAutoScalingGroupsInput, optFns ...func(*asg.Options)) (*asg.DescribeAutoScalingGroupsOutput, error) } -// Describe the autoscaling group(s) -func GetAutoScalingGroups(c context.Context, api AutoscalingAPI, input *asg.DescribeAutoScalingGroupsInput) (*asg.DescribeAutoScalingGroupsOutput, error) { +// GetAutoScalingGroups Get the autoscaling group(s) for a given nodegroup +func GetAutoScalingGroups(c context.Context, api AwsAsgApi, input *asg.DescribeAutoScalingGroupsInput) (*asg.DescribeAutoScalingGroupsOutput, error) { return api.DescribeAutoScalingGroups(c, input) } -// CreateAWSNodegroupSpec will attempt to determine how the nodegroup is defined -// and map that back into objects for cluster-api and cluster-api-provider-aws -// -// This function will output both a MachinePool and an AWSManagedMachinepool object -func (f *Function) CreateAWSNodegroupSpec(ac *awsconfig) (err error) { - var ( - res *eks.ListNodegroupsOutput - cfg aws.Config - ) - - if cfg, err = xfnaws.Config(ac.region, ac.providerConfigRef); err != nil { - err = errors.Wrap(err, "failed to load aws config for assume role") - return - } - - eksclient := eks.NewFromConfig(cfg) - ec2client := ec2.NewFromConfig(cfg) - asgclient := asg.NewFromConfig(cfg) - - clusterInput := &eks.ListNodegroupsInput{ - ClusterName: ac.cluster, - } - - if res, err = GetNodegroups(context.TODO(), eksclient, clusterInput); err != nil { - err = errors.Wrap(err, fmt.Sprintf("failed to load nodegroups for cluster %q", *ac.cluster)) - return - } - - for _, nodegroup := range res.Nodegroups { - nodegroupInput := &eks.DescribeNodegroupInput{ - ClusterName: ac.cluster, - NodegroupName: &nodegroup, - } - var group *eks.DescribeNodegroupOutput - if group, err = DescribeNodegroup(context.TODO(), eksclient, nodegroupInput); err != nil { - f.log.Debug("AWSAPI", "cannot describe nodegroup", nodegroup, "cluster", *ac.cluster, "error", err) - continue - } - - var ng *expinfrav2.AWSManagedMachinePoolSpec - if ng, err = f.nodegroupToCapiObject(group.Nodegroup, ec2client, asgclient); err != nil { - f.log.Debug("AWSAPI", "cannot create nodegroup", nodegroup, "cluster", *ac.cluster, "error", err) - continue - } - - ac.labels["giantswarm.io/machine-pool"] = nodegroup - var nodegroupName string = fmt.Sprintf("%s-awsmanagedmachinepool-%s", *ac.cluster, nodegroup) - f.log.Info("AWSAPI", "Creating nodegroup", nodegroupName) - var awsmmp expinfrav2.AWSManagedMachinePool = expinfrav2.AWSManagedMachinePool{ - TypeMeta: metav1.TypeMeta{ - Kind: "AWSManagedMachinePool", - APIVersion: "infrastructure.cluster.x-k8s.io/v1beta2", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: nodegroupName, - Namespace: *ac.namespace, - Labels: ac.labels, - Annotations: ac.annotations, - }, - Spec: *ng, - Status: expinfrav2.AWSManagedMachinePoolStatus{ - Ready: true, - Replicas: int32(len(ng.ProviderIDList)), - LaunchTemplateID: group.Nodegroup.LaunchTemplate.Id, - LaunchTemplateVersion: group.Nodegroup.LaunchTemplate.Version, - }, - } - - var dataSecretName string = "" - var machinepoolName string = fmt.Sprintf("%s-machinepool-%s", *ac.cluster, nodegroup) - f.log.Info("AWSAPI", "Creating machinepool", machinepoolName) - var machinepool *capiinfra.MachineDeployment = &capiinfra.MachineDeployment{ - TypeMeta: metav1.TypeMeta{ - Kind: "MachinePool", - APIVersion: "cluster.x-k8s.io/v1beta1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: machinepoolName, - Namespace: *ac.namespace, - Labels: ac.labels, - Annotations: ac.annotations, - }, - Spec: capiinfra.MachineDeploymentSpec{ - Replicas: &awsmmp.Status.Replicas, - ClusterName: *ac.cluster, - Template: capiinfra.MachineTemplateSpec{ - Spec: capiinfra.MachineSpec{ - ClusterName: *ac.cluster, - Bootstrap: capiinfra.Bootstrap{ - DataSecretName: &dataSecretName, - }, - InfrastructureRef: v1.ObjectReference{ - Kind: "AWSManagedMachinePool", - APIVersion: "infrastructure.cluster.x-k8s.io/v1beta2", - Namespace: *ac.namespace, - Name: nodegroupName, - }, - }, - }, - }, - } - - var awsobject, mpobject *unstructured.Unstructured - if awsobject, err = composite.ToUnstructuredKubernetesObject(awsmmp, ac.composite.Spec.ClusterProviderConfigRef, ac.composite.Spec.ObjectDeletionPolicy); err != nil { - f.log.Debug("failed to convert nodegroup", nodegroupName, "cluster", *ac.cluster, "error", err, "object", awsmmp) - continue - } - - f.log.Info("Adding nodegroup to required resources", "nodegroup", nodegroupName) - if err = ac.composed.AddDesired(nodegroupName, awsobject); err != nil { - f.log.Debug("failed to add nodegroup", nodegroupName, "cluster", *ac.cluster, "error", err, "object", awsobject) - continue - } - - if mpobject, err = composite.ToUnstructuredKubernetesObject(machinepool, ac.composite.Spec.ClusterProviderConfigRef, ac.composite.Spec.ObjectDeletionPolicy); err != nil { - f.log.Debug("failed to convert machinepool", machinepoolName, "cluster", *ac.cluster, "error", err, "object", machinepool) - continue - } - - f.log.Info("Adding machinepool to required resources", "machinepool", nodegroupName) - if err = ac.composed.AddDesired(machinepoolName, mpobject); err != nil { - f.log.Debug("failed to add machinepool", machinepoolName, "cluster", *ac.cluster, "error", err, "object", mpobject) - continue - } - } - return nil -} - -// Pull all the information together to create a AWSManagedMachinePool object -func (f *Function) nodegroupToCapiObject(group *types.Nodegroup, ec2client *ec2.Client, asgclient *asg.Client) (pool *expinfrav2.AWSManagedMachinePoolSpec, err error) { - pool = &expinfrav2.AWSManagedMachinePoolSpec{} - var ( - asgName string - asg *asgtypes.AutoScalingGroup - asgLaunchTemplate *expinfrav2.AWSLaunchTemplate - ) - - if group.Resources != nil { - asgName = *group.Resources.AutoScalingGroups[0].Name - } - - if asg, asgLaunchTemplate, err = getAutoscaling(asgName, asgclient, ec2client); err != nil { - if asg == nil { - return nil, err - } +var ( + getEc2Client = func(cfg aws.Config) AwsEc2Api { + return ec2.NewFromConfig(cfg) } - pool.AMIType = (*expinfrav2.ManagedMachineAMIType)(&group.AmiType) - pool.AvailabilityZones = asg.AvailabilityZones - if pool.AWSLaunchTemplate, err = getLaunchTemplate(group.LaunchTemplate, ec2client); err != nil { - f.log.Debug("AWSAPI", "AWSLaunchTemplate error", err) + getEksClient = func(cfg aws.Config) AwsEksApi { + return eks.NewFromConfig(cfg) } - if pool.AWSLaunchTemplate == nil { - pool.InstanceType = &group.InstanceTypes[0] - } else { - if pool.AWSLaunchTemplate.InstanceType == "" { - pool.AWSLaunchTemplate.InstanceType = group.InstanceTypes[0] - } - - f.log.Debug("Autoscaling", "AWSLaunchTemplate", pool.AWSLaunchTemplate) - f.log.Debug("Autoscaling", "asgLaunchTemplate", asgLaunchTemplate) - if asgLaunchTemplate != nil { - if pool.AWSLaunchTemplate.AMI.ID == nil { - pool.AWSLaunchTemplate.AMI.ID = asgLaunchTemplate.AMI.ID - } - - if pool.AWSLaunchTemplate.IamInstanceProfile == "" { - pool.AWSLaunchTemplate.IamInstanceProfile = asgLaunchTemplate.IamInstanceProfile - } - } - } - - var capacityTypes map[types.CapacityTypes]expinfrav2.ManagedMachinePoolCapacityType = map[types.CapacityTypes]expinfrav2.ManagedMachinePoolCapacityType{ - types.CapacityTypesOnDemand: expinfrav2.ManagedMachinePoolCapacityTypeOnDemand, - types.CapacityTypesSpot: expinfrav2.ManagedMachinePoolCapacityTypeOnDemand, - } - ct := capacityTypes[group.CapacityType] - - pool.CapacityType = &ct - pool.DiskSize = group.DiskSize - pool.EKSNodegroupName = *group.NodegroupName - pool.Labels = group.Labels - - for _, instance := range asg.Instances { - var pid string = fmt.Sprintf("aws:///%s/%s", *instance.AvailabilityZone, *instance.InstanceId) - pool.ProviderIDList = append(pool.ProviderIDList, pid) - } - - if group.RemoteAccess != nil { - pool.RemoteAccess = &expinfrav2.ManagedRemoteAccess{ - SSHKeyName: group.RemoteAccess.Ec2SshKey, - SourceSecurityGroups: group.RemoteAccess.SourceSecurityGroups, - } - } - - pool.RoleName = strings.Split(*group.NodeRole, "/")[1] - - if group.ScalingConfig != nil { - pool.Scaling = &expinfrav2.ManagedMachinePoolScaling{ - MinSize: group.ScalingConfig.MinSize, - MaxSize: group.ScalingConfig.MaxSize, - } - } - - pool.SubnetIDs = group.Subnets - for _, taint := range group.Taints { - t := expinfrav2.Taint{ - Effect: expinfrav2.TaintEffect(taint.Effect), - Key: *taint.Key, - Value: *taint.Value, - } - pool.Taints = append(pool.Taints, t) + getAsgClient = func(cfg aws.Config) AwsAsgApi { + return asg.NewFromConfig(cfg) } - pool.UpdateConfig = &expinfrav2.UpdateConfig{} - { - if group.UpdateConfig.MaxUnavailable != nil { - var max int = int(*group.UpdateConfig.MaxUnavailable) - pool.UpdateConfig.MaxUnavailable = &max - } - - if group.UpdateConfig.MaxUnavailablePercentage != nil { - var max int = int(*group.UpdateConfig.MaxUnavailablePercentage) - pool.UpdateConfig.MaxUnavailablePercentage = &max - } - } - - return -} - -func getAutoscaling(name string, client *asg.Client, ec2client *ec2.Client) (*asgtypes.AutoScalingGroup, *expinfrav2.AWSLaunchTemplate, error) { - var ( - res *asg.DescribeAutoScalingGroupsOutput - err error - ) - input := asg.DescribeAutoScalingGroupsInput{ - AutoScalingGroupNames: []string{ - name, - }, - } - - if res, err = GetAutoScalingGroups(context.TODO(), client, &input); err != nil { - return nil, nil, err + awsConfig = func(region, provider *string) (aws.Config, error) { + return xfnaws.Config(region, provider) } - - var ( - autoscaling asgtypes.AutoScalingGroup = res.AutoScalingGroups[0] - asglt *asgtypes.LaunchTemplateSpecification - lt types.LaunchTemplateSpecification - asgLaunchTemplate *expinfrav2.AWSLaunchTemplate - ) - - if autoscaling.MixedInstancesPolicy != nil && autoscaling.MixedInstancesPolicy.LaunchTemplate != nil { - asglt = autoscaling.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification - var latest string = "$Default" - - if asglt.Version == nil { - asglt.Version = &latest - } - lt = types.LaunchTemplateSpecification{ - Id: asglt.LaunchTemplateId, - Name: asglt.LaunchTemplateName, - Version: asglt.Version, - } - asgLaunchTemplate, err = getLaunchTemplate(<, ec2client) - } - - return &autoscaling, asgLaunchTemplate, err -} +) diff --git a/awsfunctions.go b/awsfunctions.go new file mode 100644 index 0000000..5982762 --- /dev/null +++ b/awsfunctions.go @@ -0,0 +1,391 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/aws-sdk-go-v2/service/eks/types" + infrav2 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + expinfrav2 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" + + "github.com/aws/aws-sdk-go-v2/aws" + asg "github.com/aws/aws-sdk-go-v2/service/autoscaling" + asgtypes "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/giantswarm/xfnlib/pkg/composite" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + capiinfra "sigs.k8s.io/cluster-api/api/v1beta1" +) + +// CreateAWSNodegroupSpec will attempt to determine how the nodegroup is defined +// and map that back into objects for cluster-api and cluster-api-provider-aws +// +// This function will output both a MachinePool and an AWSManagedMachinepool object +func (f *Function) CreateAWSNodegroupSpec(ac *XrConfig) (err error) { + var ( + res *eks.ListNodegroupsOutput + cfg aws.Config + ) + + if cfg, err = awsConfig(ac.region, ac.providerConfigRef); err != nil { + err = errors.Wrap(err, "failed to load aws config for assume role") + return + } + + eksclient := getEksClient(cfg) + ec2client := getEc2Client(cfg) + asgclient := getAsgClient(cfg) + + clusterInput := &eks.ListNodegroupsInput{ + ClusterName: ac.cluster, + } + + if res, err = GetNodegroups(context.TODO(), eksclient, clusterInput); err != nil { + err = errors.Wrap(err, fmt.Sprintf("failed to load nodegroups for cluster %q", *ac.cluster)) + return + } + + for _, nodegroup := range res.Nodegroups { + nodegroupInput := &eks.DescribeNodegroupInput{ + ClusterName: ac.cluster, + NodegroupName: &nodegroup, + } + var group *eks.DescribeNodegroupOutput + if group, err = DescribeNodegroup(context.TODO(), eksclient, nodegroupInput); err != nil { + f.log.Debug("AWSAPI", "cannot describe nodegroup", nodegroup, "cluster", *ac.cluster, "error", err) + continue + } + + var ng *expinfrav2.AWSManagedMachinePoolSpec + if ng, err = f.nodegroupToCapiObject(group.Nodegroup, ec2client, asgclient); err != nil { + f.log.Debug("AWSAPI", "cannot create nodegroup", nodegroup, "cluster", *ac.cluster, "error", err) + continue + } + + ac.labels["giantswarm.io/machine-pool"] = nodegroup + var nodegroupName string = fmt.Sprintf("%s-awsmanagedmachinepool-%s", *ac.cluster, nodegroup) + f.log.Info("AWSAPI", "Creating nodegroup", nodegroupName) + var awsmmp expinfrav2.AWSManagedMachinePool = expinfrav2.AWSManagedMachinePool{ + TypeMeta: metav1.TypeMeta{ + Kind: "AWSManagedMachinePool", + APIVersion: "infrastructure.cluster.x-k8s.io/v1beta2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: nodegroupName, + Namespace: *ac.namespace, + Labels: ac.labels, + Annotations: ac.annotations, + }, + Spec: *ng, + Status: expinfrav2.AWSManagedMachinePoolStatus{ + Ready: true, + Replicas: int32(len(ng.ProviderIDList)), + LaunchTemplateID: group.Nodegroup.LaunchTemplate.Id, + LaunchTemplateVersion: group.Nodegroup.LaunchTemplate.Version, + }, + } + + var dataSecretName string = "" + var machinepoolName string = fmt.Sprintf("%s-machinepool-%s", *ac.cluster, nodegroup) + f.log.Info("AWSAPI", "Creating machinepool", machinepoolName) + var machinepool *capiinfra.MachineDeployment = &capiinfra.MachineDeployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachinePool", + APIVersion: "cluster.x-k8s.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: machinepoolName, + Namespace: *ac.namespace, + Labels: ac.labels, + Annotations: ac.annotations, + }, + Spec: capiinfra.MachineDeploymentSpec{ + Replicas: &awsmmp.Status.Replicas, + ClusterName: *ac.cluster, + Template: capiinfra.MachineTemplateSpec{ + Spec: capiinfra.MachineSpec{ + ClusterName: *ac.cluster, + Bootstrap: capiinfra.Bootstrap{ + DataSecretName: &dataSecretName, + }, + InfrastructureRef: v1.ObjectReference{ + Kind: "AWSManagedMachinePool", + APIVersion: "infrastructure.cluster.x-k8s.io/v1beta2", + Namespace: *ac.namespace, + Name: nodegroupName, + }, + }, + }, + }, + } + + var awsobject, mpobject *unstructured.Unstructured + if awsobject, err = composite.ToUnstructuredKubernetesObject(awsmmp, ac.composite.Spec.ClusterProviderConfigRef, ac.composite.Spec.ObjectDeletionPolicy); err != nil { + f.log.Debug("failed to convert nodegroup", nodegroupName, "cluster", *ac.cluster, "error", err, "object", awsmmp) + continue + } + + f.log.Info("Adding nodegroup to required resources", "nodegroup", nodegroupName) + if err = ac.composed.AddDesired(nodegroupName, awsobject); err != nil { + f.log.Debug("failed to add nodegroup", nodegroupName, "cluster", *ac.cluster, "error", err, "object", awsobject) + continue + } + + if mpobject, err = composite.ToUnstructuredKubernetesObject(machinepool, ac.composite.Spec.ClusterProviderConfigRef, ac.composite.Spec.ObjectDeletionPolicy); err != nil { + f.log.Debug("failed to convert machinepool", machinepoolName, "cluster", *ac.cluster, "error", err, "object", machinepool) + continue + } + + f.log.Info("Adding machinepool to required resources", "machinepool", nodegroupName) + if err = ac.composed.AddDesired(machinepoolName, mpobject); err != nil { + f.log.Debug("failed to add machinepool", machinepoolName, "cluster", *ac.cluster, "error", err, "object", mpobject) + continue + } + } + return nil +} + +// Pull all the information together to create a AWSManagedMachinePool object +func (f *Function) nodegroupToCapiObject(group *types.Nodegroup, ec2client AwsEc2Api, asgclient AwsAsgApi) (pool *expinfrav2.AWSManagedMachinePoolSpec, err error) { + pool = &expinfrav2.AWSManagedMachinePoolSpec{} + var ( + asgName string + asg *asgtypes.AutoScalingGroup + asgLaunchTemplate *expinfrav2.AWSLaunchTemplate + ) + + if group.Resources != nil { + asgName = *group.Resources.AutoScalingGroups[0].Name + } + + if asg, asgLaunchTemplate, err = getAutoscaling(asgName, asgclient, ec2client); err != nil { + if asg == nil { + return nil, err + } + } + pool.AMIType = (*expinfrav2.ManagedMachineAMIType)(&group.AmiType) + pool.AvailabilityZones = asg.AvailabilityZones + + if pool.AWSLaunchTemplate, err = getLaunchTemplate(group.LaunchTemplate, ec2client); err != nil { + f.log.Debug("AWSAPI", "AWSLaunchTemplate error", err) + } + + if pool.AWSLaunchTemplate == nil { + pool.InstanceType = &group.InstanceTypes[0] + } else { + if pool.AWSLaunchTemplate.InstanceType == "" { + pool.AWSLaunchTemplate.InstanceType = group.InstanceTypes[0] + } + + f.log.Debug("Autoscaling", "AWSLaunchTemplate", pool.AWSLaunchTemplate) + f.log.Debug("Autoscaling", "asgLaunchTemplate", asgLaunchTemplate) + if asgLaunchTemplate != nil { + if pool.AWSLaunchTemplate.AMI.ID == nil { + pool.AWSLaunchTemplate.AMI.ID = asgLaunchTemplate.AMI.ID + } + + if pool.AWSLaunchTemplate.IamInstanceProfile == "" { + pool.AWSLaunchTemplate.IamInstanceProfile = asgLaunchTemplate.IamInstanceProfile + } + } + } + + var capacityTypes map[types.CapacityTypes]expinfrav2.ManagedMachinePoolCapacityType = map[types.CapacityTypes]expinfrav2.ManagedMachinePoolCapacityType{ + types.CapacityTypesOnDemand: expinfrav2.ManagedMachinePoolCapacityTypeOnDemand, + types.CapacityTypesSpot: expinfrav2.ManagedMachinePoolCapacityTypeOnDemand, + } + ct := capacityTypes[group.CapacityType] + + pool.CapacityType = &ct + pool.DiskSize = group.DiskSize + pool.EKSNodegroupName = *group.NodegroupName + pool.Labels = group.Labels + + for _, instance := range asg.Instances { + var pid string = fmt.Sprintf("aws:///%s/%s", *instance.AvailabilityZone, *instance.InstanceId) + pool.ProviderIDList = append(pool.ProviderIDList, pid) + } + + if group.RemoteAccess != nil { + pool.RemoteAccess = &expinfrav2.ManagedRemoteAccess{ + SSHKeyName: group.RemoteAccess.Ec2SshKey, + SourceSecurityGroups: group.RemoteAccess.SourceSecurityGroups, + } + } + + pool.RoleName = strings.Split(*group.NodeRole, "/")[1] + + if group.ScalingConfig != nil { + pool.Scaling = &expinfrav2.ManagedMachinePoolScaling{ + MinSize: group.ScalingConfig.MinSize, + MaxSize: group.ScalingConfig.MaxSize, + } + } + + pool.SubnetIDs = group.Subnets + for _, taint := range group.Taints { + t := expinfrav2.Taint{ + Effect: expinfrav2.TaintEffect(taint.Effect), + Key: *taint.Key, + Value: *taint.Value, + } + pool.Taints = append(pool.Taints, t) + } + + pool.UpdateConfig = &expinfrav2.UpdateConfig{} + { + if group.UpdateConfig != nil { + if group.UpdateConfig.MaxUnavailable != nil { + var max int = int(*group.UpdateConfig.MaxUnavailable) + pool.UpdateConfig.MaxUnavailable = &max + } + + if group.UpdateConfig.MaxUnavailablePercentage != nil { + var max int = int(*group.UpdateConfig.MaxUnavailablePercentage) + pool.UpdateConfig.MaxUnavailablePercentage = &max + } + } + } + + return +} + +func getAutoscaling(name string, client AwsAsgApi, ec2client AwsEc2Api) (*asgtypes.AutoScalingGroup, *expinfrav2.AWSLaunchTemplate, error) { + var ( + res *asg.DescribeAutoScalingGroupsOutput + err error + ) + input := asg.DescribeAutoScalingGroupsInput{ + AutoScalingGroupNames: []string{ + name, + }, + } + + if res, err = GetAutoScalingGroups(context.TODO(), client, &input); err != nil { + return nil, nil, err + } + + var ( + autoscaling asgtypes.AutoScalingGroup = res.AutoScalingGroups[0] + asglt *asgtypes.LaunchTemplateSpecification + lt types.LaunchTemplateSpecification + asgLaunchTemplate *expinfrav2.AWSLaunchTemplate + ) + + if autoscaling.MixedInstancesPolicy != nil && autoscaling.MixedInstancesPolicy.LaunchTemplate != nil { + asglt = autoscaling.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification + var latest string = "$Latest" + + if asglt.Version == nil { + asglt.Version = &latest + } + lt = types.LaunchTemplateSpecification{ + Id: asglt.LaunchTemplateId, + Name: asglt.LaunchTemplateName, + Version: asglt.Version, + } + asgLaunchTemplate, err = getLaunchTemplate(<, ec2client) + } + + return &autoscaling, asgLaunchTemplate, err +} + +func getLaunchTemplate(base *types.LaunchTemplateSpecification, client AwsEc2Api) (*expinfrav2.AWSLaunchTemplate, error) { + if base == nil { + // NOOP here + return nil, nil + } + + var ( + res *ec2.DescribeLaunchTemplateVersionsOutput + template expinfrav2.AWSLaunchTemplate + err error + ) + + input := ec2.DescribeLaunchTemplateVersionsInput{ + LaunchTemplateId: base.Id, + Versions: []string{ + *base.Version, + }, + } + + if res, err = DescribeLaunchTemplateVersions(context.TODO(), client, &input); err != nil { + return nil, err + } + + if len(res.LaunchTemplateVersions) != 1 { + return nil, fmt.Errorf("wrong count for launch templates for template %s", *base.Name) + } + + template.Name = *base.Name + template.VersionNumber = res.LaunchTemplateVersions[0].VersionNumber + + var data *ec2types.ResponseLaunchTemplateData = res.LaunchTemplateVersions[0].LaunchTemplateData + template.InstanceType = string(data.InstanceType) + template.SSHKeyName = data.KeyName + + template.AMI = infrav2.AMIReference{ + ID: data.ImageId, + } + + if data.IamInstanceProfile != nil { + if data.IamInstanceProfile.Name != nil && !strings.HasPrefix(*data.IamInstanceProfile.Name, "eks-") { + template.IamInstanceProfile = *data.IamInstanceProfile.Name + } else if data.IamInstanceProfile.Arn != nil { + template.IamInstanceProfile = *data.IamInstanceProfile.Arn + } + } + + if data.InstanceMarketOptions != nil && data.InstanceMarketOptions.SpotOptions != nil { + template.SpotMarketOptions = &infrav2.SpotMarketOptions{ + MaxPrice: data.InstanceMarketOptions.SpotOptions.MaxPrice, + } + } + + if len(data.BlockDeviceMappings) > 0 { + var ( + device ec2types.LaunchTemplateBlockDeviceMapping = data.BlockDeviceMappings[0] + throughput int64 = int64(*device.Ebs.Throughput) + ) + template.RootVolume = &infrav2.Volume{ + DeviceName: *device.DeviceName, + Encrypted: device.Ebs.Encrypted, + IOPS: int64(*device.Ebs.Iops), + Size: int64(*device.Ebs.VolumeSize), + Throughput: &throughput, + Type: infrav2.VolumeType(device.Ebs.VolumeType), + } + } + + // This is necessary to ensure duplicate security groups are not + // added into the list as there is no sanitation on the AWS launch + // template to prevent it. + // AWS will simply store whatever you provide. + var sgs []string = make([]string, 0) + for _, id := range data.SecurityGroupIds { + var added bool = false + for _, v := range sgs { + if id == v { + added = true + } + } + if !added { + sgs = append(sgs, id) + } + } + + template.AdditionalSecurityGroups = make([]infrav2.AWSResourceReference, 0, len(sgs)) + for i := range sgs { + template.AdditionalSecurityGroups = append(template.AdditionalSecurityGroups, infrav2.AWSResourceReference{ + ID: &sgs[i], + }) + } + + return &template, nil +} diff --git a/fn.go b/fn.go index 6cdac3c..9601663 100644 --- a/fn.go +++ b/fn.go @@ -7,7 +7,7 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/errors" fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" "github.com/crossplane/function-sdk-go/response" - "github.com/giantswarm/crossplane-fn-describe-nodegroups/input/v1beta1" + "github.com/giantswarm/crossplane-fn-describe-nodegroups/pkg/input/v1beta1" "github.com/giantswarm/xfnlib/pkg/composite" ) @@ -20,7 +20,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ rsp = response.To(req, response.DefaultTTL) var ( - ac awsconfig = awsconfig{} + ac XrConfig = XrConfig{} input v1beta1.Input ) if ac.composed, err = composite.New(req, &input, &ac.composite); err != nil { @@ -28,14 +28,13 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ return rsp, nil } - f.log.Info("input spec", "input", input) if input.Spec == nil { - response.Normal(rsp, "Waiting for spec") + response.Fatal(rsp, &composite.MissingSpec{}) return rsp, nil } if _, ok := ac.composed.ObservedComposed[input.Spec.ClusterRef]; !ok { - response.Normal(rsp, "Waiting for resource") + response.Normal(rsp, "waiting for resource") return rsp, nil } @@ -44,11 +43,10 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ ac.region = &ac.composite.Spec.Region ac.providerConfigRef = &ac.composite.Spec.CloudProviderConfigRef - ac.annotations = map[string]string{ - "cluster.x-k8s.io/managed-by": "crossplane", - } ac.labels = ac.composite.Metadata.Labels - // Merge in the additional labels for kubernetes resources + if ac.labels == nil { + ac.labels = make(map[string]string) + } for k, v := range ac.composite.Spec.KubernetesAdditionalLabels { ac.labels[k] = v } @@ -56,18 +54,19 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ ac.labels["giantswarm.io/cluster"] = *ac.cluster var provider string = ac.composite.Spec.CompositionSelector.MatchLabels.Provider - f.log.Info(provider) - switch strings.ToLower(provider) { - case "aws": - f.log.Info("discovered aws provider", composedName, req.GetMeta().GetTag()) - if err = f.CreateAWSNodegroupSpec(&ac); err != nil { - response.Fatal(rsp, errors.Wrapf(err, "cannot get desired composite resources from %T", req)) - return rsp, nil + { + switch strings.ToLower(provider) { + case "aws": + f.log.Info("discovered aws provider", composedName, req.GetMeta().GetTag()) + if err = f.CreateAWSNodegroupSpec(&ac); err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot create composed resources from %T", req)) + return rsp, nil + } + case "azure": + f.log.Info("Azure provider is not yet implemented") + case "gcp": + f.log.Info("GCP provider is not yet implemented") } - case "azure": - f.log.Info("Azure provider is not yet implemented") - case "gcp": - f.log.Info("GCP provider is not yet implemented") } if err = ac.composed.ToResponse(rsp); err != nil { diff --git a/fn_test.go b/fn_test.go index b3c6d2e..50959d5 100644 --- a/fn_test.go +++ b/fn_test.go @@ -2,21 +2,264 @@ package main import ( "context" + "fmt" "testing" + "time" + "github.com/aws/aws-sdk-go-v2/aws" + asg "github.com/aws/aws-sdk-go-v2/service/autoscaling" + asgtypes "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" + "github.com/aws/aws-sdk-go-v2/service/ec2" + ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/aws/aws-sdk-go-v2/service/eks/types" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/durationpb" "github.com/crossplane/crossplane-runtime/pkg/logging" - "github.com/giantswarm/crossplane-fn-describe-nodegroups/input/v1beta1" + "github.com/giantswarm/crossplane-fn-describe-nodegroups/pkg/input/v1beta1" fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" "github.com/crossplane/function-sdk-go/resource" "github.com/crossplane/function-sdk-go/response" ) +var ( + xr = `{"apiVersion": "example.org/v1","kind": "XR", "spec": { + "clusterName": "test","clusterProviderConfigRef": "thingy", + "regionOrLocation": "placey", "deletionPolicy": "Delete", + "objectDeletionPolicy": "Delete", "claimRef":{"namespace":"default"}, + "labels": {"test": "label","anothertest": "label" + },"kubernetesAdditionalLabels": {"foo": "bar"}, + "compositionSelector": {"matchLabels": {"provider": "aws"}}}}` + + cluster = `{"apiVersion": "eks.aws.upbound.io/v1beta1", "kind": "Cluster", + "metadata": {"annotations": {"crossplane.io/external-name": "example", + "crossplane.io/composition-resource-name": "eks-cluster"}, "labels": { + "crossplane.io/claim-name": "example"}}, "managementPolicies": ["Observe"], + "spec": {"forProvider": {"region": "eu-central-1"},"providerConfigRef": { + "name": "example"},"writeConnectionSecretToRef": {"namespace": "example"}}, + "status": {"atProvider": {"vpcConfig": [{"vpcId": "vpc-12345678", + "subnetIds": ["subnet-123456"]}]}}}` + + nodepool = `{"apiVersion":"kubernetes.crossplane.io/v1alpha1", + "kind":"Object","metadata":{"labels":{"foo":"bar", + "giantswarm.io/cluster":"test","giantswarm.io/machine-pool":"ng-12345", + "cluster.x-k8s.io/cluster-name":"test"},"name":"test-awsmanagedmachinepool-ng-12345"}, + "spec":{"deletionPolicy":"Delete","forProvider":{"manifest":{ + "apiVersion":"infrastructure.cluster.x-k8s.io/v1beta2","kind":"AWSManagedMachinePool", + "metadata":{"labels":{"foo":"bar","cluster.x-k8s.io/cluster-name":"test", + "giantswarm.io/cluster":"test","giantswarm.io/machine-pool":"ng-12345"}, + "name":"test-awsmanagedmachinepool-ng-12345","namespace":"default", + "creationTimestamp":null},"spec":{"amiType":"AL2_x86_64", + "availabilityZones":["eu-central-1a","eu-central-1c","eu-central-1b"], + "awsLaunchTemplate":{"additionalSecurityGroups":[{"id":"sg-11111111111111111"}, + {"id":"sg-22222222222222222"}], + "ami":{"id":"ami-0ab553a58389ae35a"}, + "instanceType":"m5.large","name":"eksctl-example-nodegroup-ng-1", + "rootVolume":{"deviceName":"/dev/xvda","iops":3000,"size":80, + "throughput":125,"type":"gp3"},"spotMarketOptions":{"maxPrice":"expensive"}, + "sshKeyName":"test-key","versionNumber":1},"capacityType":"onDemand", + "eksNodegroupName":"ng-12345","providerIDList":["aws:///eu-central-1c/i-1111111111111111", + "aws:///eu-central-1a/i-2222222222222222","aws:///eu-central-1b/i-3333333333333333"], + "roleName":"eksctl-example-nodegroup-NodeInstanceRole-123456789123", + "scaling":{"maxSize":3,"minSize":1},"subnetIDs":["subnet-1111111111111111", + "subnet-2222222222222222","subnet-3333333333333333"],"updateConfig":{"maxUnavailable":1}}, + "status":{"launchTemplateID":"lt-123456","launchTemplateVersion":"2","ready":true, + "replicas":3}}},"providerConfigRef":{"name":"thingy"},"writeConnectionSecretToRef":{ + "name":"test-awsmanagedmachinepool-ng-12345","namespace":"default"}}}` + + machinepool = `{"apiVersion":"kubernetes.crossplane.io/v1alpha1","kind":"Object", + "metadata":{"labels":{"foo":"bar","giantswarm.io/cluster":"test", + "giantswarm.io/machine-pool":"ng-12345","cluster.x-k8s.io/cluster-name":"test"}, + "name":"test-machinepool-ng-12345"},"spec":{"deletionPolicy":"Delete", + "forProvider":{"manifest":{"apiVersion":"cluster.x-k8s.io/v1beta1", + "kind":"MachinePool","metadata":{"labels":{"foo":"bar", + "giantswarm.io/cluster":"test","giantswarm.io/machine-pool":"ng-12345", + "cluster.x-k8s.io/cluster-name":"test"},"name":"test-machinepool-ng-12345", + "namespace":"default","creationTimestamp":null},"spec":{"clusterName":"test", + "replicas":3,"selector":{},"template":{"metadata":{},"spec":{ + "bootstrap":{"dataSecretName":""},"clusterName":"test", + "infrastructureRef":{"apiVersion":"infrastructure.cluster.x-k8s.io/v1beta2", + "kind":"AWSManagedMachinePool","name":"test-awsmanagedmachinepool-ng-12345", + "namespace":"default"}}}},"status":{"availableReplicas":0,"readyReplicas":0, + "replicas":0,"unavailableReplicas":0,"updatedReplicas":0}}}, + "providerConfigRef":{"name":"thingy"},"writeConnectionSecretToRef":{ + "name":"test-machinepool-ng-12345","namespace":"default"}}}` +) + +type NodegroupErrorMock struct { + eks.Client +} + +func (n *NodegroupErrorMock) DescribeNodegroup(ctx context.Context, + params *eks.DescribeNodegroupInput, + optFns ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) { + return nil, fmt.Errorf("just a failure") +} + +func (e *NodegroupErrorMock) ListNodegroups(ctx context.Context, + params *eks.ListNodegroupsInput, + optFns ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) { + return nil, fmt.Errorf("just a failure") +} + +type NodegroupMock struct { + eks.Client +} + +func (n *NodegroupMock) DescribeNodegroup(ctx context.Context, + params *eks.DescribeNodegroupInput, + optFns ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) { + return &eks.DescribeNodegroupOutput{ + Nodegroup: &types.Nodegroup{ + AmiType: "AL2_x86_64", + Version: aws.String("1.25"), + CapacityType: types.CapacityTypesOnDemand, + ClusterName: aws.String("test"), + CreatedAt: aws.Time(time.Now()), + //DiskSize: aws.Int32(80), + InstanceTypes: nil, + LaunchTemplate: &types.LaunchTemplateSpecification{ + Id: aws.String("lt-123456"), + Name: aws.String("eksctl-example-nodegroup-ng-1"), + Version: aws.String("2"), + }, + NodeRole: aws.String("role/eksctl-example-nodegroup-NodeInstanceRole-123456789123"), + ScalingConfig: &types.NodegroupScalingConfig{ + DesiredSize: aws.Int32(1), + MaxSize: aws.Int32(3), + MinSize: aws.Int32(1), + }, + NodegroupArn: aws.String("arn::123456:some-role"), + Subnets: []string{ + "subnet-1111111111111111", + "subnet-2222222222222222", + "subnet-3333333333333333", + }, + NodegroupName: aws.String("ng-12345"), + UpdateConfig: &types.NodegroupUpdateConfig{ + MaxUnavailable: aws.Int32(1), + }, + }, + }, nil +} + +func (e *NodegroupMock) ListNodegroups(ctx context.Context, + params *eks.ListNodegroupsInput, + optFns ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) { + return &eks.ListNodegroupsOutput{ + Nodegroups: []string{ + "ng-12345", + }, + }, nil +} + +type EmptyEc2Mock struct{} + +func (e *EmptyEc2Mock) DescribeLaunchTemplates(ctx context.Context, + params *ec2.DescribeLaunchTemplatesInput, + optFns ...func(*ec2.Options)) (*ec2.DescribeLaunchTemplatesOutput, error) { + return nil, nil +} + +func (e *EmptyEc2Mock) DescribeLaunchTemplateVersions(ctx context.Context, + params *ec2.DescribeLaunchTemplateVersionsInput, + optFns ...func(*ec2.Options)) (*ec2.DescribeLaunchTemplateVersionsOutput, error) { + return nil, nil +} + +type ValidEc2Mock struct{} + +func (e *ValidEc2Mock) DescribeLaunchTemplates(ctx context.Context, + params *ec2.DescribeLaunchTemplatesInput, + optFns ...func(*ec2.Options)) (*ec2.DescribeLaunchTemplatesOutput, error) { + return nil, nil +} + +func (e *ValidEc2Mock) DescribeLaunchTemplateVersions(ctx context.Context, + params *ec2.DescribeLaunchTemplateVersionsInput, + optFns ...func(*ec2.Options)) (*ec2.DescribeLaunchTemplateVersionsOutput, error) { + return &ec2.DescribeLaunchTemplateVersionsOutput{ + LaunchTemplateVersions: []ec2types.LaunchTemplateVersion{ + { + LaunchTemplateData: &ec2types.ResponseLaunchTemplateData{ + InstanceMarketOptions: &ec2types.LaunchTemplateInstanceMarketOptions{ + SpotOptions: &ec2types.LaunchTemplateSpotMarketOptions{ + MaxPrice: aws.String("expensive"), + }, + }, + BlockDeviceMappings: []ec2types.LaunchTemplateBlockDeviceMapping{ + { + DeviceName: aws.String("/dev/xvda"), + Ebs: &ec2types.LaunchTemplateEbsBlockDevice{ + Iops: aws.Int32(3000), + VolumeSize: aws.Int32(80), + Throughput: aws.Int32(125), + VolumeType: ec2types.VolumeTypeGp3, + }, + }, + }, + IamInstanceProfile: &ec2types.LaunchTemplateIamInstanceProfileSpecification{ + Name: aws.String("eks-123456789123456789"), + }, + SecurityGroupIds: []string{ + "sg-11111111111111111", + "sg-22222222222222222", + }, + InstanceType: ec2types.InstanceTypeM5Large, + KeyName: aws.String("test-key"), + ImageId: aws.String("ami-0ab553a58389ae35a"), + }, + VersionNumber: aws.Int64(1), + }, + }, + }, nil +} + +type EmptyAsgMock struct{} + +func (e *EmptyAsgMock) DescribeAutoScalingGroups(ctx context.Context, + params *asg.DescribeAutoScalingGroupsInput, + optFns ...func(*asg.Options)) (*asg.DescribeAutoScalingGroupsOutput, error) { + return nil, nil +} + +type ValidAsgMock struct{} + +func (e *ValidAsgMock) DescribeAutoScalingGroups(ctx context.Context, + params *asg.DescribeAutoScalingGroupsInput, + optFns ...func(*asg.Options)) (*asg.DescribeAutoScalingGroupsOutput, error) { + return &asg.DescribeAutoScalingGroupsOutput{ + AutoScalingGroups: []asgtypes.AutoScalingGroup{ + { + AutoScalingGroupName: aws.String("example"), + AvailabilityZones: []string{ + "eu-central-1a", + "eu-central-1c", + "eu-central-1b", + }, + Instances: []asgtypes.Instance{ + { + InstanceId: aws.String("i-1111111111111111"), + AvailabilityZone: aws.String("eu-central-1c"), + }, + { + InstanceId: aws.String("i-2222222222222222"), + AvailabilityZone: aws.String("eu-central-1a"), + }, + { + InstanceId: aws.String("i-3333333333333333"), + AvailabilityZone: aws.String("eu-central-1b"), + }, + }, + }, + }, + }, nil +} + func TestRunFunction(t *testing.T) { type args struct { @@ -28,13 +271,21 @@ func TestRunFunction(t *testing.T) { err error } + type mocks struct { + ec2 func(cfg aws.Config) AwsEc2Api + eks func(cfg aws.Config) AwsEksApi + asg func(cfg aws.Config) AwsAsgApi + aws func(region, provider *string) (aws.Config, error) + } + cases := map[string]struct { reason string args args want want + mocks mocks }{ - "NormalResponseWaitingWhenClusterRefDoesntExist": { - reason: "When cluster ref is undefined, we get a normal response", + "input is undefined": { + reason: "When cluster ref is undefined, we get a fatal response", args: args{ req: &fnv1beta1.RunFunctionRequest{ Input: resource.MustStructObject(&v1beta1.Input{}), @@ -43,6 +294,31 @@ func TestRunFunction(t *testing.T) { want: want{ rsp: &fnv1beta1.RunFunctionResponse{ Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: "object does not contain spec field", + }, + }, + }, + }, + mocks: mocks{}, + }, + "spec is empty": { + reason: "the function returns normal if spec is not yet populated", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Meta: &fnv1beta1.RequestMeta{Tag: "hello"}, + Input: resource.MustStructJSON(`{ + "apiVersion": "dummy.fn.crossplane.io", + "kind": "Input", + "spec": {} + }`), + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, Results: []*fnv1beta1.Result{ { Severity: fnv1beta1.Severity_SEVERITY_NORMAL, @@ -51,10 +327,157 @@ func TestRunFunction(t *testing.T) { }, }, }, + mocks: mocks{}, + }, + "function returns fatal if nodegroups cannot be loaded": { + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + Spec: &v1beta1.Spec{ + ClusterRef: "eks-cluster", + }, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "eks-cluster": { + Resource: resource.MustStructJSON(cluster), + }, + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "eks-cluster": { + Resource: resource.MustStructJSON(cluster), + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: "cannot create composed resources from *v1beta1.RunFunctionRequest: failed to load nodegroups for cluster \"test\": just a failure", + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "eks-cluster": { + Resource: resource.MustStructJSON(cluster), + }, + }, + }, + }, + }, + mocks: mocks{ + aws: func(region, provider *string) (aws.Config, error) { + return aws.Config{}, nil + }, + eks: func(_ aws.Config) AwsEksApi { + return &NodegroupErrorMock{} + }, + ec2: func(_ aws.Config) AwsEc2Api { + return &EmptyEc2Mock{} + }, + asg: func(_ aws.Config) AwsAsgApi { + return &EmptyAsgMock{} + }, + }, + }, + "function returns success when nodepool is created": { + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject(&v1beta1.Input{ + Spec: &v1beta1.Spec{ + ClusterRef: "eks-cluster", + }, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "eks-cluster": { + Resource: resource.MustStructJSON(cluster), + }, + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "eks-cluster": { + Resource: resource.MustStructJSON(cluster), + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_NORMAL, + Message: "Successful run", + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "eks-cluster": { + Resource: resource.MustStructJSON(cluster), + }, + "test-awsmanagedmachinepool-ng-12345": { + Ready: fnv1beta1.Ready_READY_TRUE, + Resource: resource.MustStructJSON(nodepool), + }, + "test-machinepool-ng-12345": { + Ready: fnv1beta1.Ready_READY_TRUE, + Resource: resource.MustStructJSON(machinepool), + }, + }, + }, + }, + }, + mocks: mocks{ + aws: func(region, provider *string) (aws.Config, error) { + return aws.Config{}, nil + }, + eks: func(_ aws.Config) AwsEksApi { + return &NodegroupMock{} + }, + ec2: func(_ aws.Config) AwsEc2Api { + return &ValidEc2Mock{} + }, + asg: func(_ aws.Config) AwsAsgApi { + return &ValidAsgMock{} + }, + }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { + // set up any required mocks + awsConfig = tc.mocks.aws + getAsgClient = tc.mocks.asg + getEc2Client = tc.mocks.ec2 + getEksClient = tc.mocks.eks + f := &Function{log: logging.NewNopLogger()} rsp, err := f.RunFunction(tc.args.ctx, tc.args.req) if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" { diff --git a/helpers.go b/helpers.go deleted file mode 100644 index b5a9661..0000000 --- a/helpers.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "context" - - "github.com/crossplane/crossplane-runtime/pkg/errors" - "github.com/giantswarm/xfnlib/pkg/composite" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - clientconfig "sigs.k8s.io/controller-runtime/pkg/client/config" -) - -func GetKubeClient() (c client.Client, err error) { - var config *rest.Config - - if config, err = clientconfig.GetConfig(); err != nil { - err = errors.Wrap(err, "cannot get cluster config") - return - } - - if c, err = client.New(config, client.Options{}); err != nil { - err = errors.Wrap(err, "failed to create cluster client") - } - - return -} - -func GetAssumeRoleArn(providerConfigRef *string) (arn *string, err error) { - var ( - unstructuredData *unstructured.Unstructured = &unstructured.Unstructured{} - cl client.Client - ) - if cl, err = GetKubeClient(); err != nil { - err = errors.Wrap(err, "error setting up kubernetes client") - return - } - // Get the provider context - unstructuredData.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "aws.upbound.io", - Kind: "ProviderConfig", - Version: "v1beta1", - }) - - if err = cl.Get(context.Background(), client.ObjectKey{ - Name: *providerConfigRef, - }, unstructuredData); err != nil { - err = errors.Wrap(err, "failed to load providerconfig") - return - } - - type _spec struct { - AssumeRoleChain []struct { - RoleARN string `json:"roleARN"` - } `json:"assumeRoleChain"` - } - - var spec _spec - if err = composite.To(unstructuredData.Object["spec"], &spec); err != nil { - err = errors.Wrapf(err, "unable to decode provider config") - return - } - - // We only care about the first in the chain here. - arn = &spec.AssumeRoleChain[0].RoleARN - return -} diff --git a/launchtemplates.go b/launchtemplates.go deleted file mode 100644 index cd58a5b..0000000 --- a/launchtemplates.go +++ /dev/null @@ -1,128 +0,0 @@ -package main - -import ( - "context" - "fmt" - "strings" - - "github.com/aws/aws-sdk-go-v2/service/ec2" - ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/aws/aws-sdk-go-v2/service/eks/types" - infrav2 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" - expinfrav2 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" -) - -// EC2API Describes the functions required to access data on the AWS EC2 api -type EC2API interface { - DescribeLaunchTemplates(ctx context.Context, - params *ec2.DescribeLaunchTemplatesInput, - optFns ...func(*ec2.Options)) (*ec2.DescribeLaunchTemplatesOutput, error) - - DescribeLaunchTemplateVersions(ctx context.Context, - params *ec2.DescribeLaunchTemplateVersionsInput, - optFns ...func(*ec2.Options)) (*ec2.DescribeLaunchTemplateVersionsOutput, error) -} - -// Get the EC2 Launch template versions for a given launch template -func DescribeLaunchTemplateVersions(c context.Context, api EC2API, input *ec2.DescribeLaunchTemplateVersionsInput) (*ec2.DescribeLaunchTemplateVersionsOutput, error) { - return api.DescribeLaunchTemplateVersions(c, input) -} - -func DescribeLaunchTemplates(c context.Context, api EC2API, input *ec2.DescribeLaunchTemplatesInput) (*ec2.DescribeLaunchTemplatesOutput, error) { - return api.DescribeLaunchTemplates(c, input) -} - -func getLaunchTemplate(base *types.LaunchTemplateSpecification, client *ec2.Client) (*expinfrav2.AWSLaunchTemplate, error) { - if base == nil { - // NOOP here - return nil, nil - } - - var ( - res *ec2.DescribeLaunchTemplateVersionsOutput - template expinfrav2.AWSLaunchTemplate - err error - ) - - input := ec2.DescribeLaunchTemplateVersionsInput{ - LaunchTemplateId: base.Id, - Versions: []string{ - *base.Version, - }, - } - - if res, err = DescribeLaunchTemplateVersions(context.TODO(), client, &input); err != nil { - return nil, err - } - - if len(res.LaunchTemplateVersions) != 1 { - return nil, fmt.Errorf("wrong count for launch templates for template %s", *base.Name) - } - - //template.Name = *res.LaunchTemplateVersions[0].LaunchTemplateName - template.Name = *base.Name - template.VersionNumber = res.LaunchTemplateVersions[0].VersionNumber - - var data *ec2types.ResponseLaunchTemplateData = res.LaunchTemplateVersions[0].LaunchTemplateData - template.InstanceType = string(data.InstanceType) - template.SSHKeyName = data.KeyName - - template.AMI = infrav2.AMIReference{ - ID: data.ImageId, - } - - if data.IamInstanceProfile != nil { - if data.IamInstanceProfile.Name != nil && !strings.HasPrefix(*data.IamInstanceProfile.Name, "eks-") { - template.IamInstanceProfile = *data.IamInstanceProfile.Name - } else if data.IamInstanceProfile.Arn != nil { - template.IamInstanceProfile = *data.IamInstanceProfile.Arn - } - } - - if data.InstanceMarketOptions != nil && data.InstanceMarketOptions.SpotOptions != nil { - template.SpotMarketOptions = &infrav2.SpotMarketOptions{ - MaxPrice: data.InstanceMarketOptions.SpotOptions.MaxPrice, - } - } - - if len(data.BlockDeviceMappings) > 0 { - var ( - device ec2types.LaunchTemplateBlockDeviceMapping = data.BlockDeviceMappings[0] - throughput int64 = int64(*device.Ebs.Throughput) - ) - template.RootVolume = &infrav2.Volume{ - DeviceName: *device.DeviceName, - Encrypted: device.Ebs.Encrypted, - IOPS: int64(*device.Ebs.Iops), - Size: int64(*device.Ebs.VolumeSize), - Throughput: &throughput, - Type: infrav2.VolumeType(device.Ebs.VolumeType), - } - } - - // This is necessary to ensure duplicate security groups are not - // added into the list as there is no sanitation on the AWS launch - // template to prevent it. - // AWS will simply store whatever you provide. - var sgs []string = make([]string, 0) - for _, id := range data.SecurityGroupIds { - var added bool = false - for _, v := range sgs { - if id == v { - added = true - } - } - if !added { - sgs = append(sgs, id) - } - } - - template.AdditionalSecurityGroups = make([]infrav2.AWSResourceReference, 0, len(sgs)) - for i := range sgs { - template.AdditionalSecurityGroups = append(template.AdditionalSecurityGroups, infrav2.AWSResourceReference{ - ID: &sgs[i], - }) - } - - return &template, nil -} diff --git a/package/composite/definition_xrobjectdefinitions.yaml b/package/composite/definition_xrobjectdefinitions.yaml new file mode 100644 index 0000000..b351b0e --- /dev/null +++ b/package/composite/definition_xrobjectdefinitions.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: xrobjectdefinitions.definition +spec: + group: definition + names: + categories: + - crossplane + - composition + - functions + - subnets + kind: XrObjectDefinition + listKind: XrObjectDefinitionList + plural: xrobjectdefinitions + singular: xrobjectdefinition + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: "XrObjectDefinition contains information about the XR \n This + type is a meta-type for defining the XRD spec as it excludes fields normally + defined as part of a standard XRD definition" + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec The specification of the XR + properties: + cloudProviderConfigRef: + description: Defines the name of the providerconfig for the cloud + provider + type: string + clusterName: + description: Defines the name of the cluster to map from + type: string + clusterProviderConfigRef: + description: Defines the name of the providerconfig used by `crossplane-contrib/provider-kubernetes` + type: string + kubernetesAdditionalLabels: + additionalProperties: + type: string + description: Additional labels to add to kubernetes resources + type: object + x-kubernetes-map-type: granular + labels: + additionalProperties: + type: string + description: Labels is a set of additional labels to be applied to + all objects + type: object + x-kubernetes-map-type: granular + objectDeletionPolicy: + description: The deletion policy for kubernetes objects + type: string + regionOrLocation: + description: Defines the region or location for cloud resources + type: string + resourceGroupName: + type: string + required: + - cloudProviderConfigRef + - clusterName + - clusterProviderConfigRef + - regionOrLocation + type: object + required: + - spec + type: object + served: true + storage: true diff --git a/pkg/composite/generate.go b/pkg/composite/generate.go new file mode 100644 index 0000000..959b9bf --- /dev/null +++ b/pkg/composite/generate.go @@ -0,0 +1,13 @@ +//go:build generate +// +build generate + +// NOTE(negz): See the below link for details on what is happening here. +// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module + +//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen paths=./v1beta1 object crd:crdVersions=v1 output:artifacts:config=../../package/composite + +package composite + +import ( + _ "sigs.k8s.io/controller-tools/cmd/controller-gen" //nolint:typecheck +) diff --git a/pkg/composite/v1beta1/definition.go b/pkg/composite/v1beta1/definition.go new file mode 100644 index 0000000..28a72a1 --- /dev/null +++ b/pkg/composite/v1beta1/definition.go @@ -0,0 +1,100 @@ +// Package v1beta1 contains the definition of the XR requirements for using this function +// +kubebuilder:object:generate=true +// +groupName=definition +// +versionName=v1beta1 +package v1beta1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +kubebuilder:object:root=false +// +kubebuilder:storageversion +// +kubebuilder:resource:categories=crossplane;composition;functions;subnets + +// XrObjectDefinition contains information about the XR +// +// This type is a meta-type for defining the XRD spec as it excludes +// fields normally defined as part of a standard XRD definition +type XrObjectDefinition struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec The specification of the XR + Spec XrClaimSpec `json:"spec"` +} + +type CompositeObject struct { + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec The specification of the XR + Spec XrSpec `json:"spec"` +} + +type XrClaimSpec struct { + // Labels is a set of additional labels to be applied to all objects + // +optional + // +mapType=granular + Labels map[string]string `json:"labels"` + + // Defines the name of the providerconfig for the cloud provider + // +kubebuilder:validation:Required + CloudProviderConfigRef string `json:"cloudProviderConfigRef"` + + // Defines the name of the providerconfig used by `crossplane-contrib/provider-kubernetes` + // +kubebuilder:validation:Required + ClusterProviderConfigRef string `json:"clusterProviderConfigRef"` + + // Defines the name of the cluster to map from + // +kubebuilder:validation:Required + ClusterName string `json:"clusterName"` + + // Defines the region or location for cloud resources + // +kubebuilder:validation:Required + Region string `json:"regionOrLocation"` + + // The deletion policy for kubernetes objects + // +kubebuilder:validation:Required + ObjectDeletionPolicy string `json:"objectDeletionPolicy,omitempty"` + + // Additional labels to add to kubernetes resources + // +optional + // +mapType=granular + KubernetesAdditionalLabels map[string]string `json:"kubernetesAdditionalLabels"` + + // AZURE ONLY The name of the resource group that the cluster is located in + // This has no effect if set for Google cloud or AWS + // +optional + ResourceGroupName string `json:"resourceGroupName,omitempty"` +} + +// XRSpec is the definition of the XR as an object +type XrSpec struct { + XrClaimSpec `json:",inline"` + // Defines the deletion policy for the XR + // +optional + DeletionPolicy string `json:"deletionPolicy"` + + // Defines a reference to the claim used by this XR + // +optional + ClaimRef ClaimRef `json:"claimRef"` + + // Defines the selector for the composition + // +optional + CompositionSelector CompositionSelector `json:"compositionSelector"` +} + +// ClaimRef stores information about the claim +type ClaimRef struct { + // The namespace the claim is stored in + Namespace string `json:"namespace"` +} + +// The selector for the composition +type CompositionSelector struct { + MatchLabels MatchLabels `json:"matchLabels"` +} + +// Labels to match on the composition for selection +type MatchLabels struct { + // The provider label used to select the composition + Provider string `json:"provider"` +} diff --git a/pkg/composite/v1beta1/zz_generated.deepcopy.go b/pkg/composite/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 0000000..d177089 --- /dev/null +++ b/pkg/composite/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,135 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import () + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimRef) DeepCopyInto(out *ClaimRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimRef. +func (in *ClaimRef) DeepCopy() *ClaimRef { + if in == nil { + return nil + } + out := new(ClaimRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompositeObject) DeepCopyInto(out *CompositeObject) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeObject. +func (in *CompositeObject) DeepCopy() *CompositeObject { + if in == nil { + return nil + } + out := new(CompositeObject) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompositionSelector) DeepCopyInto(out *CompositionSelector) { + *out = *in + out.MatchLabels = in.MatchLabels +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositionSelector. +func (in *CompositionSelector) DeepCopy() *CompositionSelector { + if in == nil { + return nil + } + out := new(CompositionSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MatchLabels) DeepCopyInto(out *MatchLabels) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchLabels. +func (in *MatchLabels) DeepCopy() *MatchLabels { + if in == nil { + return nil + } + out := new(MatchLabels) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *XrClaimSpec) DeepCopyInto(out *XrClaimSpec) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.KubernetesAdditionalLabels != nil { + in, out := &in.KubernetesAdditionalLabels, &out.KubernetesAdditionalLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new XrClaimSpec. +func (in *XrClaimSpec) DeepCopy() *XrClaimSpec { + if in == nil { + return nil + } + out := new(XrClaimSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *XrObjectDefinition) DeepCopyInto(out *XrObjectDefinition) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new XrObjectDefinition. +func (in *XrObjectDefinition) DeepCopy() *XrObjectDefinition { + if in == nil { + return nil + } + out := new(XrObjectDefinition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *XrSpec) DeepCopyInto(out *XrSpec) { + *out = *in + in.XrClaimSpec.DeepCopyInto(&out.XrClaimSpec) + out.ClaimRef = in.ClaimRef + out.CompositionSelector = in.CompositionSelector +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new XrSpec. +func (in *XrSpec) DeepCopy() *XrSpec { + if in == nil { + return nil + } + out := new(XrSpec) + in.DeepCopyInto(out) + return out +} diff --git a/input/generate.go b/pkg/input/generate.go similarity index 91% rename from input/generate.go rename to pkg/input/generate.go index 551cc2a..1330437 100644 --- a/input/generate.go +++ b/pkg/input/generate.go @@ -4,7 +4,7 @@ // NOTE(negz): See the below link for details on what is happening here. // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module -//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen paths=./v1beta1 object crd:crdVersions=v1 output:artifacts:config=../package/input +//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen paths=./v1beta1 object crd:crdVersions=v1 output:artifacts:config=../../package/input package input diff --git a/input/v1beta1/input.go b/pkg/input/v1beta1/input.go similarity index 100% rename from input/v1beta1/input.go rename to pkg/input/v1beta1/input.go diff --git a/input/v1beta1/zz_generated.deepcopy.go b/pkg/input/v1beta1/zz_generated.deepcopy.go similarity index 100% rename from input/v1beta1/zz_generated.deepcopy.go rename to pkg/input/v1beta1/zz_generated.deepcopy.go diff --git a/types.go b/types.go index 53972c2..dfc8bf6 100644 --- a/types.go +++ b/types.go @@ -3,6 +3,7 @@ package main import ( "github.com/crossplane/crossplane-runtime/pkg/logging" fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" + xfc "github.com/giantswarm/crossplane-fn-describe-nodegroups/pkg/composite/v1beta1" "github.com/giantswarm/xfnlib/pkg/composite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -27,35 +28,10 @@ type ConnectionSecretRef struct { // EksImportXRObject is the information we are going to pull from the XR type EksImportXRObject struct { Metadata metav1.ObjectMeta `json:"metadata"` - Spec XRSpec `json:"spec"` + Spec xfc.XrSpec `json:"spec"` } -type XRSpec struct { - KubernetesAdditionalLabels map[string]string `json:"kubernetesAdditionalLabels"` - Labels map[string]string `json:"labels"` - CloudProviderConfigRef string `json:"cloudProviderConfigRef"` - ClusterName string `json:"clusterName"` - DeletionPolicy string `json:"deletionPolicy"` - ClusterProviderConfigRef string `json:"clusterProviderConfigRef"` - Region string `json:"regionOrLocation"` - ResourceGroupName string `json:"resourceGroupName,omitempty"` - ObjectDeletionPolicy string `json:"objectDeletionPolicy,omitempty"` - ClaimRef struct { - Namespace string `json:"namespace"` - } `json:"claimRef"` - - CompositionSelector struct { - MatchLabels struct { - Provider string `json:"provider"` - } `json:"matchLabels"` - } `json:"compositionSelector"` -} - -type XRStatus struct { - AWSRoleArn string `json:"roleArn"` -} - -type awsconfig struct { +type XrConfig struct { cluster, namespace, region, providerConfigRef *string labels, annotations map[string]string composed *composite.Composition