From 162906eada1dd3ca37ef94a123f02d2503d1136b Mon Sep 17 00:00:00 2001 From: Cesar Wong Date: Tue, 16 Feb 2021 12:54:04 -0500 Subject: [PATCH 1/3] Implement BYO infra API changes --- api/fixtures/example.go | 44 +- api/v1alpha1/hosted_controlplane.go | 3 + api/v1alpha1/hostedcluster_types.go | 51 ++ api/v1alpha1/nodepool_types.go | 17 +- api/v1alpha1/zz_generated.deepcopy.go | 89 +- cmd/cluster/create.go | 91 +- cmd/infra/aws/create_iam.go | 2 +- cmd/infra/aws/ec2.go | 44 +- ...ypershift.openshift.io_hostedclusters.yaml | 136 ++- ...hift.openshift.io_hostedcontrolplanes.yaml | 116 +++ .../hypershift.openshift.io_nodepools.yaml | 39 +- .../hostedcontrolplane/assets/assets.go | 2 + .../hostedcontrolplane/assets/aws/aws.conf | 5 + .../cluster-config-v1-configmap.yaml | 10 +- .../cluster-infrastructure-02-config.yaml | 17 +- .../csi-driver-cloud-credentials.yaml | 12 + .../assets/install-config/install-config.yaml | 13 + .../assets/kube-apiserver/config.yaml | 4 + .../kube-apiserver-configmap.yaml | 4 + .../kube-apiserver-deployment.yaml | 14 + .../kube-controller-manager/config.yaml | 2 + .../kube-controller-manager-configmap.yaml | 4 + .../kube-controller-manager-deployment.yaml | 20 + .../cluster-dns-02-config.yaml | 6 - .../cluster-infrastructure-02-config.yaml | 16 - .../cluster-network-02-config.yaml | 14 - .../cluster-proxy-01-config.yaml | 8 - .../machine-config-server/install-config.yaml | 7 - .../machine-config-server-configmap.yaml | 10 +- .../assets/openvpn/server.conf | 2 + .../hostedcontrolplane/assets/openvpn/worker | 1 + .../hostedcontrolplane_controller.go | 100 ++- .../hostedcontrolplane/render/funcs.go | 17 + .../render/kube_apiserver.go | 44 +- .../hostedcontrolplane/render/manifests.go | 1 + .../hostedcontrolplane/render/types.go | 10 + docs/api.md | 14 +- go.mod | 1 + go.sum | 5 + .../hostedcluster/hostedcluster_controller.go | 9 - .../controlplaneoperator/manifests.go | 13 +- .../hostedcluster/manifests/manifests.go | 8 +- .../controllers/machineimage/interface.go | 8 + .../machineimage/static/4.7/rhcos-amd64.json | 163 ++++ .../controllers/machineimage/static/assets.go | 15 + .../machineimage/static/provider.go | 35 + .../machineimage/static/provider_test.go | 27 + .../nodepool/nodepool_controller.go | 87 +- hypershift-operator/main.go | 4 +- .../api/v1alpha3/zz_generated.deepcopy.go | 4 +- .../api/v1alpha4/zz_generated.deepcopy.go | 4 +- .../v1alpha3/zz_generated.deepcopy.go | 2 +- vendor/gopkg.in/ini.v1/.gitignore | 6 + vendor/gopkg.in/ini.v1/.travis.yml | 20 + vendor/gopkg.in/ini.v1/LICENSE | 191 +++++ vendor/gopkg.in/ini.v1/Makefile | 15 + vendor/gopkg.in/ini.v1/README.md | 39 + vendor/gopkg.in/ini.v1/data_source.go | 74 ++ vendor/gopkg.in/ini.v1/deprecated.go | 25 + vendor/gopkg.in/ini.v1/error.go | 34 + vendor/gopkg.in/ini.v1/file.go | 418 +++++++++ vendor/gopkg.in/ini.v1/helper.go | 24 + vendor/gopkg.in/ini.v1/ini.go | 166 ++++ vendor/gopkg.in/ini.v1/key.go | 801 ++++++++++++++++++ vendor/gopkg.in/ini.v1/parser.go | 526 ++++++++++++ vendor/gopkg.in/ini.v1/section.go | 256 ++++++ vendor/gopkg.in/ini.v1/struct.go | 603 +++++++++++++ .../k8s.io/apimachinery/pkg/util/rand/rand.go | 127 +++ vendor/k8s.io/client-go/util/retry/OWNERS | 4 + vendor/k8s.io/client-go/util/retry/util.go | 105 +++ vendor/modules.txt | 5 + 71 files changed, 4558 insertions(+), 255 deletions(-) create mode 100644 control-plane-operator/controllers/hostedcontrolplane/assets/aws/aws.conf create mode 100644 control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/csi-driver-cloud-credentials.yaml create mode 100644 control-plane-operator/controllers/hostedcontrolplane/assets/install-config/install-config.yaml delete mode 100644 control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-dns-02-config.yaml delete mode 100644 control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-infrastructure-02-config.yaml delete mode 100644 control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-network-02-config.yaml delete mode 100644 control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-proxy-01-config.yaml delete mode 100644 control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/install-config.yaml create mode 100644 hypershift-operator/controllers/machineimage/interface.go create mode 100644 hypershift-operator/controllers/machineimage/static/4.7/rhcos-amd64.json create mode 100644 hypershift-operator/controllers/machineimage/static/assets.go create mode 100644 hypershift-operator/controllers/machineimage/static/provider.go create mode 100644 hypershift-operator/controllers/machineimage/static/provider_test.go create mode 100644 vendor/gopkg.in/ini.v1/.gitignore create mode 100644 vendor/gopkg.in/ini.v1/.travis.yml create mode 100644 vendor/gopkg.in/ini.v1/LICENSE create mode 100644 vendor/gopkg.in/ini.v1/Makefile create mode 100644 vendor/gopkg.in/ini.v1/README.md create mode 100644 vendor/gopkg.in/ini.v1/data_source.go create mode 100644 vendor/gopkg.in/ini.v1/deprecated.go create mode 100644 vendor/gopkg.in/ini.v1/error.go create mode 100644 vendor/gopkg.in/ini.v1/file.go create mode 100644 vendor/gopkg.in/ini.v1/helper.go create mode 100644 vendor/gopkg.in/ini.v1/ini.go create mode 100644 vendor/gopkg.in/ini.v1/key.go create mode 100644 vendor/gopkg.in/ini.v1/parser.go create mode 100644 vendor/gopkg.in/ini.v1/section.go create mode 100644 vendor/gopkg.in/ini.v1/struct.go create mode 100644 vendor/k8s.io/apimachinery/pkg/util/rand/rand.go create mode 100644 vendor/k8s.io/client-go/util/retry/OWNERS create mode 100644 vendor/k8s.io/client-go/util/retry/util.go diff --git a/api/fixtures/example.go b/api/fixtures/example.go index cb8dc5565371..96535149dc1e 100644 --- a/api/fixtures/example.go +++ b/api/fixtures/example.go @@ -35,6 +35,20 @@ type ExampleOptions struct { AWSCredentials []byte SSHKey []byte NodePoolReplicas int + InfraID string + ComputeCIDR string + + AWS ExampleAWSOptions +} + +type ExampleAWSOptions struct { + Region string + Zone string + VPCID string + SubnetID string + SecurityGroupID string + InstanceProfile string + InstanceType string } func (o ExampleOptions) Resources() *ExampleResources { @@ -104,11 +118,31 @@ func (o ExampleOptions) Resources() *ExampleResources { Image: o.ReleaseImage, }, InitialComputeReplicas: o.NodePoolReplicas, - ServiceCIDR: "172.31.0.0/16", - PodCIDR: "10.132.0.0/14", - PullSecret: corev1.LocalObjectReference{Name: pullSecret.Name}, - ProviderCreds: corev1.LocalObjectReference{Name: awsCredsSecret.Name}, - SSHKey: corev1.LocalObjectReference{Name: sshKeySecret.Name}, + Networking: hyperv1.ClusterNetworking{ + ServiceCIDR: "172.31.0.0/16", + PodCIDR: "10.132.0.0/14", + MachineCIDR: o.ComputeCIDR, + }, + InfraID: o.InfraID, + PullSecret: corev1.LocalObjectReference{Name: pullSecret.Name}, + ProviderCreds: corev1.LocalObjectReference{Name: awsCredsSecret.Name}, + SSHKey: corev1.LocalObjectReference{Name: sshKeySecret.Name}, + Platform: hyperv1.PlatformSpec{ + AWS: &hyperv1.AWSPlatformSpec{ + Region: o.AWS.Region, + NodePoolDefaults: &hyperv1.AWSNodePoolPlatform{ + InstanceType: o.AWS.InstanceType, + InstanceProfile: o.AWS.InstanceProfile, + Subnet: &hyperv1.AWSResourceReference{ + ID: &o.AWS.SubnetID, + }, + SecurityGroups: []hyperv1.AWSResourceReference{ + {ID: &o.AWS.SecurityGroupID}, + }, + Zone: o.AWS.Zone, + }, + }, + }, }, } diff --git a/api/v1alpha1/hosted_controlplane.go b/api/v1alpha1/hosted_controlplane.go index 1c5c185fc2e7..349bbc72736f 100644 --- a/api/v1alpha1/hosted_controlplane.go +++ b/api/v1alpha1/hosted_controlplane.go @@ -29,8 +29,11 @@ type HostedControlPlaneSpec struct { PullSecret corev1.LocalObjectReference `json:"pullSecret"` ServiceCIDR string `json:"serviceCIDR"` PodCIDR string `json:"podCIDR"` + MachineCIDR string `json:"machineCIDR"` SSHKey corev1.LocalObjectReference `json:"sshKey"` ProviderCreds corev1.LocalObjectReference `json:"providerCreds"` + InfraID string `json:"infraID"` + Platform PlatformSpec `json:"platform"` } type ConditionType string diff --git a/api/v1alpha1/hostedcluster_types.go b/api/v1alpha1/hostedcluster_types.go index 179e828c9141..96408fd6ce96 100644 --- a/api/v1alpha1/hostedcluster_types.go +++ b/api/v1alpha1/hostedcluster_types.go @@ -42,10 +42,61 @@ type HostedClusterSpec struct { SSHKey corev1.LocalObjectReference `json:"sshKey"` + // ProviderCreds is a reference to a secret containing cloud account info ProviderCreds corev1.LocalObjectReference `json:"providerCreds"` + // Networking contains network-specific settings for this cluster + Networking ClusterNetworking `json:"networking"` + + Platform PlatformSpec `json:"platform"` + + // InfraID is used to identify the cluster in cloud platforms + InfraID string `json:"infraID,omitempty"` +} + +type ClusterNetworking struct { ServiceCIDR string `json:"serviceCIDR"` PodCIDR string `json:"podCIDR"` + MachineCIDR string `json:"machineCIDR"` +} + +type PlatformSpec struct { + // AWS contains AWS-specific settings for the HostedCluster + // +optional + AWS *AWSPlatformSpec `json:"aws,omitempty"` +} + +type AWSPlatformSpec struct { + // Region is the AWS region for the cluster + Region string `json:"region"` + + // VPC specifies the VPC used for the cluster + VPC string `json:"vpc"` + + // NodePoolDefaults specifies the default platform + // +optional + NodePoolDefaults *AWSNodePoolPlatform `json:"nodePoolDefaults,omitempty"` + + // ServiceEndpoints list contains custom endpoints which will override default + // service endpoint of AWS Services. + // There must be only one ServiceEndpoint for a service. + // +optional + ServiceEndpoints []AWSServiceEndpoint `json:"serviceEndpoints,omitempty"` +} + +// AWSServiceEndpoint stores the configuration for services to +// override existing defaults of AWS Services. +type AWSServiceEndpoint struct { + // Name is the name of the AWS service. + // This must be provided and cannot be empty. + Name string `json:"name"` + + // URL is fully qualified URI with scheme https, that overrides the default generated + // endpoint for a client. + // This must be provided and cannot be empty. + // + // +kubebuilder:validation:Pattern=`^https://` + URL string `json:"url"` } type Release struct { diff --git a/api/v1alpha1/nodepool_types.go b/api/v1alpha1/nodepool_types.go index dd7cc43136e0..b385c3f20d14 100644 --- a/api/v1alpha1/nodepool_types.go +++ b/api/v1alpha1/nodepool_types.go @@ -107,9 +107,20 @@ type NodePoolPlatform struct { type AWSNodePoolPlatform struct { // InstanceType defines the ec2 instance type. // eg. m4-large - InstanceType string `json:"instanceType"` - InstanceProfile string `json:"instanceProfile,omitempty"` - Subnet *AWSResourceReference `json:"subnet,omitempty"` + InstanceType string `json:"instanceType"` + InstanceProfile string `json:"instanceProfile,omitempty"` + // Subnet is the subnet to use for instances + // +optional + Subnet *AWSResourceReference `json:"subnet,omitempty"` + // AMI is the image id to use + // +optional + AMI string `json:"ami,omitempty"` + // SecurityGroups is the set of security groups to associate with nodepool machines + // +optional + SecurityGroups []AWSResourceReference `json:"securityGroups,omitempty"` + // Zone is the availability zone where the instances are created + // +optional + Zone string `json:"zone,omitempty"` } // AWSResourceReference is a reference to a specific AWS resource by ID, ARN, or filters. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 85ab75589fcf..8cb0437dfa86 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -50,6 +50,13 @@ func (in *AWSNodePoolPlatform) DeepCopyInto(out *AWSNodePoolPlatform) { *out = new(AWSResourceReference) (*in).DeepCopyInto(*out) } + if in.SecurityGroups != nil { + in, out := &in.SecurityGroups, &out.SecurityGroups + *out = make([]AWSResourceReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSNodePoolPlatform. @@ -62,6 +69,31 @@ func (in *AWSNodePoolPlatform) DeepCopy() *AWSNodePoolPlatform { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSPlatformSpec) DeepCopyInto(out *AWSPlatformSpec) { + *out = *in + if in.NodePoolDefaults != nil { + in, out := &in.NodePoolDefaults, &out.NodePoolDefaults + *out = new(AWSNodePoolPlatform) + (*in).DeepCopyInto(*out) + } + if in.ServiceEndpoints != nil { + in, out := &in.ServiceEndpoints, &out.ServiceEndpoints + *out = make([]AWSServiceEndpoint, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSPlatformSpec. +func (in *AWSPlatformSpec) DeepCopy() *AWSPlatformSpec { + if in == nil { + return nil + } + out := new(AWSPlatformSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AWSResourceReference) DeepCopyInto(out *AWSResourceReference) { *out = *in @@ -94,6 +126,36 @@ func (in *AWSResourceReference) DeepCopy() *AWSResourceReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSServiceEndpoint) DeepCopyInto(out *AWSServiceEndpoint) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSServiceEndpoint. +func (in *AWSServiceEndpoint) DeepCopy() *AWSServiceEndpoint { + if in == nil { + return nil + } + out := new(AWSServiceEndpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterNetworking) DeepCopyInto(out *ClusterNetworking) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterNetworking. +func (in *ClusterNetworking) DeepCopy() *ClusterNetworking { + if in == nil { + return nil + } + out := new(ClusterNetworking) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterVersionStatus) DeepCopyInto(out *ClusterVersionStatus) { *out = *in @@ -232,7 +294,7 @@ func (in *HostedCluster) DeepCopyInto(out *HostedCluster) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -293,6 +355,8 @@ func (in *HostedClusterSpec) DeepCopyInto(out *HostedClusterSpec) { out.PullSecret = in.PullSecret out.SSHKey = in.SSHKey out.ProviderCreds = in.ProviderCreds + out.Networking = in.Networking + in.Platform.DeepCopyInto(&out.Platform) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostedClusterSpec. @@ -335,7 +399,7 @@ func (in *HostedControlPlane) DeepCopyInto(out *HostedControlPlane) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -411,6 +475,7 @@ func (in *HostedControlPlaneSpec) DeepCopyInto(out *HostedControlPlaneSpec) { out.PullSecret = in.PullSecret out.SSHKey = in.SSHKey out.ProviderCreds = in.ProviderCreds + in.Platform.DeepCopyInto(&out.Platform) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostedControlPlaneSpec. @@ -624,6 +689,26 @@ func (in *NodePoolStatus) DeepCopy() *NodePoolStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformSpec) DeepCopyInto(out *PlatformSpec) { + *out = *in + if in.AWS != nil { + in, out := &in.AWS, &out.AWS + *out = new(AWSPlatformSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformSpec. +func (in *PlatformSpec) DeepCopy() *PlatformSpec { + if in == nil { + return nil + } + out := new(PlatformSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Release) DeepCopyInto(out *Release) { *out = *in diff --git a/cmd/cluster/create.go b/cmd/cluster/create.go index 33c5655cf9bd..1da232e99fe9 100644 --- a/cmd/cluster/create.go +++ b/cmd/cluster/create.go @@ -3,6 +3,7 @@ package cluster import ( "bytes" "context" + "encoding/json" "fmt" "io/ioutil" "os" @@ -10,9 +11,11 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" + utilrand "k8s.io/apimachinery/pkg/util/rand" hyperapi "github.com/openshift/hypershift/api" apifixtures "github.com/openshift/hypershift/api/fixtures" + awsinfra "github.com/openshift/hypershift/cmd/infra/aws" "github.com/openshift/hypershift/version" cr "sigs.k8s.io/controller-runtime" @@ -20,14 +23,19 @@ import ( ) type Options struct { - Namespace string - Name string - ReleaseImage string - PullSecretFile string - AWSCredentialsFile string - SSHKeyFile string - NodePoolReplicas int - Render bool + Namespace string + Name string + ReleaseImage string + PullSecretFile string + AWSCredentialsFile string + SSHKeyFile string + NodePoolReplicas int + Render bool + InfraID string + InfrastructureJSON string + WorkerInstanceProfile string + InstanceType string + Region string } func NewCreateCommand() *cobra.Command { @@ -47,14 +55,19 @@ func NewCreateCommand() *cobra.Command { } opts := Options{ - Namespace: "clusters", - Name: "example", - ReleaseImage: releaseImage, - PullSecretFile: "", - AWSCredentialsFile: "", - SSHKeyFile: filepath.Join(os.Getenv("HOME"), ".ssh", "id_rsa.pub"), - NodePoolReplicas: 2, - Render: false, + Namespace: "clusters", + Name: "example", + ReleaseImage: releaseImage, + PullSecretFile: "", + AWSCredentialsFile: "", + SSHKeyFile: filepath.Join(os.Getenv("HOME"), ".ssh", "id_rsa.pub"), + NodePoolReplicas: 2, + Render: false, + InfrastructureJSON: "", + WorkerInstanceProfile: "hypershift-worker-profile", + Region: "us-east-1", + InfraID: "", + InstanceType: "m4.large", } cmd.Flags().StringVar(&opts.Namespace, "namespace", opts.Namespace, "A namespace to contain the generated resources") @@ -65,6 +78,11 @@ func NewCreateCommand() *cobra.Command { cmd.Flags().StringVar(&opts.SSHKeyFile, "ssh-key", opts.SSHKeyFile, "Path to an SSH key file") cmd.Flags().IntVar(&opts.NodePoolReplicas, "node-pool-replicas", opts.NodePoolReplicas, "If >0, create a default NodePool with this many replicas") cmd.Flags().BoolVar(&opts.Render, "render", opts.Render, "Render output as YAML to stdout instead of applying") + cmd.Flags().StringVar(&opts.InfrastructureJSON, "infra-json", opts.InfrastructureJSON, "Path to file containing infrastructure information for the cluster. If not specified, infrastructure will be created") + cmd.Flags().StringVar(&opts.WorkerInstanceProfile, "instance-profile", opts.WorkerInstanceProfile, "Name of the AWS instance profile to use for workers.") + cmd.Flags().StringVar(&opts.Region, "region", opts.Region, "Region to use for AWS infrastructure.") + cmd.Flags().StringVar(&opts.InfraID, "infra-id", opts.InfraID, "Infrastructure ID to use for AWS resources.") + cmd.Flags().StringVar(&opts.InstanceType, "instance-type", opts.InstanceType, "Instance type for AWS instances.") cmd.MarkFlagRequired("pull-secret") cmd.MarkFlagRequired("aws-creds") @@ -85,6 +103,32 @@ func NewCreateCommand() *cobra.Command { if len(opts.ReleaseImage) == 0 { return fmt.Errorf("release-image flag is required if default can not be fetched") } + var infra *awsinfra.CreateInfraOutput + if len(opts.InfrastructureJSON) > 0 { + rawInfra, err := ioutil.ReadFile(opts.InfrastructureJSON) + if err != nil { + panic(err) + } + infra = &awsinfra.CreateInfraOutput{} + if err = json.Unmarshal(rawInfra, infra); err != nil { + panic(err) + } + } + if infra == nil { + infraID := opts.InfraID + if len(infraID) == 0 && infra == nil { + infraID = generateID(opts.Name) + } + opt := awsinfra.CreateInfraOptions{ + Region: opts.Region, + InfraID: infraID, + AWSCredentialsFile: opts.AWSCredentialsFile, + } + infra, err = opt.CreateInfra() + if err != nil { + panic(err) + } + } exampleObjects := apifixtures.ExampleOptions{ Namespace: opts.Namespace, @@ -94,6 +138,17 @@ func NewCreateCommand() *cobra.Command { AWSCredentials: awsCredentials, SSHKey: sshKey, NodePoolReplicas: opts.NodePoolReplicas, + InfraID: infra.InfraID, + ComputeCIDR: infra.ComputeCIDR, + AWS: apifixtures.ExampleAWSOptions{ + Region: infra.Region, + Zone: infra.Zone, + VPCID: infra.VPCID, + SubnetID: infra.PrivateSubnetID, + SecurityGroupID: infra.SecurityGroupID, + InstanceProfile: opts.WorkerInstanceProfile, + InstanceType: opts.InstanceType, + }, }.Resources().AsObjects() switch { @@ -141,3 +196,7 @@ func apply(ctx context.Context, objects []crclient.Object) error { } return nil } + +func generateID(name string) string { + return fmt.Sprintf("%s-%s", name, utilrand.String(5)) +} diff --git a/cmd/infra/aws/create_iam.go b/cmd/infra/aws/create_iam.go index 292405c93b22..c8c88d282585 100644 --- a/cmd/infra/aws/create_iam.go +++ b/cmd/infra/aws/create_iam.go @@ -25,7 +25,7 @@ func NewCreateIAMCommand() *cobra.Command { opts := CreateIAMOptions{ Region: "us-east-1", - ProfileName: "worker-profile", + ProfileName: "hypershift-worker-profile", } cmd.Flags().StringVar(&opts.AWSCredentialsFile, "aws-creds", opts.AWSCredentialsFile, "Path to an AWS credentials file (required)") diff --git a/cmd/infra/aws/ec2.go b/cmd/infra/aws/ec2.go index ee5ffff5ec4d..d231cba5848c 100644 --- a/cmd/infra/aws/ec2.go +++ b/cmd/infra/aws/ec2.go @@ -5,10 +5,16 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2/ec2iface" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" +) + +const ( + InvalidNATGatewayError = "InvalidNatGatewayID.NotFound" ) func (o *CreateInfraOptions) firstZone(client ec2iface.EC2API) (string, error) { @@ -277,21 +283,6 @@ func (o *CreateInfraOptions) CreateNATGateway(client ec2iface.EC2API, publicSubn } natGateway = gatewayResult.NatGateway } - // Wait for NAT gateway to become available - err = wait.Poll(5*time.Second, 1*time.Minute, func() (bool, error) { - natgw, err := o.existingNATGateway(client, natGatewayName) - if err != nil { - return false, err - } - if natgw != nil { - return true, nil - } - return false, nil - }) - if err != nil { - return "", fmt.Errorf("NAT gateway failed to become available: %w", err) - } - natGatewayID := aws.StringValue(natGateway.NatGatewayId) return natGatewayID, nil } @@ -337,10 +328,25 @@ func (o *CreateInfraOptions) CreatePrivateRouteTable(client ec2iface.EC2API, vpc } } if !o.hasNATGatewayRoute(routeTable, natGatewayID) { - _, err = client.CreateRoute(&ec2.CreateRouteInput{ - RouteTableId: routeTable.RouteTableId, - NatGatewayId: aws.String(natGatewayID), - DestinationCidrBlock: aws.String("0.0.0.0/0"), + isRetriable := func(err error) bool { + if awsErr, ok := err.(awserr.Error); ok { + return awsErr.Code() == InvalidNATGatewayError + } + return false + } + backoff := wait.Backoff{ + Steps: 20, + Duration: 3 * time.Second, + Factor: 5.0, + Jitter: 0.1, + } + err = retry.OnError(backoff, isRetriable, func() error { + _, err = client.CreateRoute(&ec2.CreateRouteInput{ + RouteTableId: routeTable.RouteTableId, + NatGatewayId: aws.String(natGatewayID), + DestinationCidrBlock: aws.String("0.0.0.0/0"), + }) + return err }) if err != nil { return "", fmt.Errorf("cannot create nat gateway route in private route table: %w", err) diff --git a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedclusters.yaml b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedclusters.yaml index a8cd1ccc776e..b5eef036aeeb 100644 --- a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedclusters.yaml +++ b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedclusters.yaml @@ -48,12 +48,136 @@ spec: spec: description: HostedClusterSpec defines the desired state of HostedCluster properties: + infraID: + description: InfraID is used to identify the cluster in cloud platforms + type: string initialComputeReplicas: type: integer - podCIDR: - type: string + networking: + description: Networking contains network-specific settings for this cluster + properties: + machineCIDR: + type: string + podCIDR: + type: string + serviceCIDR: + type: string + required: + - machineCIDR + - podCIDR + - serviceCIDR + type: object + platform: + properties: + aws: + description: AWS contains AWS-specific settings for the HostedCluster + properties: + nodePoolDefaults: + description: NodePoolDefaults specifies the default platform + properties: + ami: + description: AMI is the image id to use + type: string + instanceProfile: + type: string + instanceType: + description: InstanceType defines the ec2 instance type. eg. m4-large + type: string + securityGroups: + description: SecurityGroups is the set of security groups to associate with nodepool machines + items: + description: AWSResourceReference is a reference to a specific AWS resource by ID, ARN, or filters. Only one of ID, ARN or Filters may be specified. Specifying more than one will result in a validation error. + properties: + arn: + description: ARN of resource + type: string + filters: + description: 'Filters is a set of key/value pairs used to identify a resource They are applied according to the rules defined by the AWS API: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Filtering.html' + items: + description: Filter is a filter used to identify an AWS resource + properties: + name: + description: Name of the filter. Filter names are case-sensitive. + type: string + values: + description: Values includes one or more filter values. Filter values are case-sensitive. + items: + type: string + type: array + required: + - name + - values + type: object + type: array + id: + description: ID of resource + type: string + type: object + type: array + subnet: + description: Subnet is the subnet to use for instances + properties: + arn: + description: ARN of resource + type: string + filters: + description: 'Filters is a set of key/value pairs used to identify a resource They are applied according to the rules defined by the AWS API: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Filtering.html' + items: + description: Filter is a filter used to identify an AWS resource + properties: + name: + description: Name of the filter. Filter names are case-sensitive. + type: string + values: + description: Values includes one or more filter values. Filter values are case-sensitive. + items: + type: string + type: array + required: + - name + - values + type: object + type: array + id: + description: ID of resource + type: string + type: object + zone: + description: Zone is the availability zone where the instances are created + type: string + required: + - instanceType + type: object + region: + description: Region is the AWS region for the cluster + type: string + serviceEndpoints: + description: ServiceEndpoints list contains custom endpoints which will override default service endpoint of AWS Services. There must be only one ServiceEndpoint for a service. + items: + description: AWSServiceEndpoint stores the configuration for services to override existing defaults of AWS Services. + properties: + name: + description: Name is the name of the AWS service. This must be provided and cannot be empty. + type: string + url: + description: URL is fully qualified URI with scheme https, that overrides the default generated endpoint for a client. This must be provided and cannot be empty. + pattern: ^https:// + type: string + required: + - name + - url + type: object + type: array + vpc: + description: VPC specifies the VPC used for the cluster + type: string + required: + - region + - vpc + type: object + type: object providerCreds: - description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. + description: ProviderCreds is a reference to a secret containing cloud account info properties: name: description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' @@ -75,8 +199,6 @@ spec: required: - image type: object - serviceCIDR: - type: string sshKey: description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. properties: @@ -86,11 +208,11 @@ spec: type: object required: - initialComputeReplicas - - podCIDR + - networking + - platform - providerCreds - pullSecret - release - - serviceCIDR - sshKey type: object status: diff --git a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedcontrolplanes.yaml b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedcontrolplanes.yaml index f79da875f167..229498fee7bb 100644 --- a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedcontrolplanes.yaml +++ b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_hostedcontrolplanes.yaml @@ -37,6 +37,119 @@ spec: spec: description: HostedControlPlaneSpec defines the desired state of HostedControlPlane properties: + infraID: + type: string + machineCIDR: + type: string + platform: + properties: + aws: + description: AWS contains AWS-specific settings for the HostedCluster + properties: + nodePoolDefaults: + description: NodePoolDefaults specifies the default platform + properties: + ami: + description: AMI is the image id to use + type: string + instanceProfile: + type: string + instanceType: + description: InstanceType defines the ec2 instance type. eg. m4-large + type: string + securityGroups: + description: SecurityGroups is the set of security groups to associate with nodepool machines + items: + description: AWSResourceReference is a reference to a specific AWS resource by ID, ARN, or filters. Only one of ID, ARN or Filters may be specified. Specifying more than one will result in a validation error. + properties: + arn: + description: ARN of resource + type: string + filters: + description: 'Filters is a set of key/value pairs used to identify a resource They are applied according to the rules defined by the AWS API: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Filtering.html' + items: + description: Filter is a filter used to identify an AWS resource + properties: + name: + description: Name of the filter. Filter names are case-sensitive. + type: string + values: + description: Values includes one or more filter values. Filter values are case-sensitive. + items: + type: string + type: array + required: + - name + - values + type: object + type: array + id: + description: ID of resource + type: string + type: object + type: array + subnet: + description: Subnet is the subnet to use for instances + properties: + arn: + description: ARN of resource + type: string + filters: + description: 'Filters is a set of key/value pairs used to identify a resource They are applied according to the rules defined by the AWS API: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Filtering.html' + items: + description: Filter is a filter used to identify an AWS resource + properties: + name: + description: Name of the filter. Filter names are case-sensitive. + type: string + values: + description: Values includes one or more filter values. Filter values are case-sensitive. + items: + type: string + type: array + required: + - name + - values + type: object + type: array + id: + description: ID of resource + type: string + type: object + zone: + description: Zone is the availability zone where the instances are created + type: string + required: + - instanceType + type: object + region: + description: Region is the AWS region for the cluster + type: string + serviceEndpoints: + description: ServiceEndpoints list contains custom endpoints which will override default service endpoint of AWS Services. There must be only one ServiceEndpoint for a service. + items: + description: AWSServiceEndpoint stores the configuration for services to override existing defaults of AWS Services. + properties: + name: + description: Name is the name of the AWS service. This must be provided and cannot be empty. + type: string + url: + description: URL is fully qualified URI with scheme https, that overrides the default generated endpoint for a client. This must be provided and cannot be empty. + pattern: ^https:// + type: string + required: + - name + - url + type: object + type: array + vpc: + description: VPC specifies the VPC used for the cluster + type: string + required: + - region + - vpc + type: object + type: object podCIDR: type: string providerCreds: @@ -65,6 +178,9 @@ spec: type: string type: object required: + - infraID + - machineCIDR + - platform - podCIDR - providerCreds - pullSecret diff --git a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_nodepools.yaml b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_nodepools.yaml index beb1c7349c82..1842e019e6a1 100644 --- a/cmd/install/assets/hypershift-operator/hypershift.openshift.io_nodepools.yaml +++ b/cmd/install/assets/hypershift-operator/hypershift.openshift.io_nodepools.yaml @@ -86,13 +86,47 @@ spec: aws: description: AWS is the configuration used when installing on AWS. properties: + ami: + description: AMI is the image id to use + type: string instanceProfile: type: string instanceType: description: InstanceType defines the ec2 instance type. eg. m4-large type: string + securityGroups: + description: SecurityGroups is the set of security groups to associate with nodepool machines + items: + description: AWSResourceReference is a reference to a specific AWS resource by ID, ARN, or filters. Only one of ID, ARN or Filters may be specified. Specifying more than one will result in a validation error. + properties: + arn: + description: ARN of resource + type: string + filters: + description: 'Filters is a set of key/value pairs used to identify a resource They are applied according to the rules defined by the AWS API: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Filtering.html' + items: + description: Filter is a filter used to identify an AWS resource + properties: + name: + description: Name of the filter. Filter names are case-sensitive. + type: string + values: + description: Values includes one or more filter values. Filter values are case-sensitive. + items: + type: string + type: array + required: + - name + - values + type: object + type: array + id: + description: ID of resource + type: string + type: object + type: array subnet: - description: AWSResourceReference is a reference to a specific AWS resource by ID, ARN, or filters. Only one of ID, ARN or Filters may be specified. Specifying more than one will result in a validation error. + description: Subnet is the subnet to use for instances properties: arn: description: ARN of resource @@ -119,6 +153,9 @@ spec: description: ID of resource type: string type: object + zone: + description: Zone is the availability zone where the instances are created + type: string required: - instanceType type: object diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/assets.go b/control-plane-operator/controllers/hostedcontrolplane/assets/assets.go index c2697cfe4d23..ff059aa6559a 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/assets.go +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/assets.go @@ -3,12 +3,14 @@ package assets import "embed" //go:embed apiserver-haproxy/* +//go:embed aws/* //go:embed cluster-bootstrap/* //go:embed cluster-version-operator/* //go:embed common/* //go:embed etcd/* //go:embed hosted-cluster-config-operator/* //go:embed ignition-configs/* +//go:embed install-config/* //go:embed kube-apiserver/* //go:embed kube-controller-manager/* //go:embed kube-scheduler/* diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/aws/aws.conf b/control-plane-operator/controllers/hostedcontrolplane/assets/aws/aws.conf new file mode 100644 index 000000000000..ca425c241c84 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/aws/aws.conf @@ -0,0 +1,5 @@ +[Global] +Zone = {{ .AWSZone }} +VPC = {{ .AWSVPCID }} +KubernetesClusterID = {{ .InfraID }} +SubnetID = {{ .AWSSubnetID }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/cluster-config-v1-configmap.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/cluster-config-v1-configmap.yaml index 1a213c7208bf..0c986a712eae 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/cluster-config-v1-configmap.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/cluster-config-v1-configmap.yaml @@ -5,12 +5,4 @@ metadata: namespace: kube-system data: install-config: | - apiVersion: v1 - # read by network-operator - controlPlane: - replicas: 1 - networking: - machineCIDR: 10.0.0.0/16 - # read by image-registry-operator and ingress-operator - platform: - none: {} \ No newline at end of file +{{ include "install-config/install-config.yaml" 4 }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/cluster-infrastructure-02-config.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/cluster-infrastructure-02-config.yaml index 165e647983ef..999afd435e44 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/cluster-infrastructure-02-config.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/cluster-infrastructure-02-config.yaml @@ -6,12 +6,19 @@ metadata: spec: cloudConfig: name: "" +{{- if eq .PlatformType "AWS" }} + platformSpec: + aws: {} +{{- end }} status: apiServerInternalURI: https://{{ .ExternalAPIDNSName }}:{{ .ExternalAPIPort }} apiServerURL: https://{{ .ExternalAPIDNSName }}:{{ .ExternalAPIPort }} etcdDiscoveryDomain: {{ .BaseDomain }} - infrastructureName: kubernetes - platform: {{ if .PlatformType }}{{ .PlatformType }}{{ else }}None {{ end }} - platformStatus: {{ if eq .PlatformType "IBMCloud" }} - type: {{ .PlatformType }} {{ else }} - type: None {{ end }} + infrastructureName: {{ .InfraID }} + platform: {{ .PlatformType }} + platformStatus: + type: {{ .PlatformType }} +{{- if eq .PlatformType "AWS" }} + aws: + region: {{ .AWSRegion }} +{{- end }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/csi-driver-cloud-credentials.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/csi-driver-cloud-credentials.yaml new file mode 100644 index 000000000000..615f0874acad --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/cluster-bootstrap/csi-driver-cloud-credentials.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ if eq .CloudProvider "aws" }}ebs-cloud-credentials{{ else }}cloud-credentials{{ end }} + namespace: openshift-cluster-csi-drivers +type: Opaque +data: +{{- if eq .CloudProvider "aws" }} + aws_access_key_id: {{ base64String (ini_value .CloudCredentials "default" "aws_access_key_id") }} + aws_secret_access_key: {{ base64String (ini_value .CloudCredentials "default" "aws_secret_access_key") }} + credentials: {{ base64String .CloudCredentials }} +{{- end }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/install-config/install-config.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/install-config/install-config.yaml new file mode 100644 index 000000000000..7a6a8e7afcf5 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/install-config/install-config.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +controlPlane: + replicas: 1 +networking: + machineNetwork: + - cidr: {{ .MachineCIDR }} +platform: +{{- if eq .CloudProvider "aws" }} + aws: + region: {{ .AWSRegion }} +{{- else }} + none: {} +{{- end }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/config.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/config.yaml index e244649bb72e..b71320215784 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/config.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/config.yaml @@ -49,6 +49,10 @@ apiServerArguments: - Node client-ca-file: - /etc/kubernetes/config/serving-ca.crt +{{- if eq .CloudProvider "aws" }} + cloud-config: + - "/etc/kubernetes/config/aws.conf" +{{- end }} cloud-provider: - "{{ .CloudProvider }}" enable-admission-plugins: diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/kube-apiserver-configmap.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/kube-apiserver-configmap.yaml index 4b7fd3c210b0..d290ac5f46db 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/kube-apiserver-configmap.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/kube-apiserver-configmap.yaml @@ -13,3 +13,7 @@ data: {{ include_pki "combined-ca.crt" 4 }} etcd-ca.crt: |- {{ include_pki "root-ca.crt" 4 }} +{{- if eq .CloudProvider "aws" }} + aws.conf: |- +{{ include "aws/aws.conf" 4 }} +{{- end}} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/kube-apiserver-deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/kube-apiserver-deployment.yaml index 9f3dfbc88716..79960b25c918 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/kube-apiserver-deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-apiserver/kube-apiserver-deployment.yaml @@ -78,6 +78,11 @@ spec: args: - "--openshift-config=/etc/kubernetes/apiserver-config/config.yaml" workingDir: /var/log/kube-apiserver + {{- if eq .CloudProvider "aws" }} + env: + - name: AWS_SHARED_CREDENTIALS_FILE + value: /etc/kubernetes/provider/credentials + {{- end }} livenessProbe: httpGet: scheme: HTTPS @@ -115,6 +120,10 @@ spec: name: logs - name: apiserver-cm mountPath: /etc/kubernetes/audit/ +{{- if .ProviderCredsSecretName }} + - name: provider-creds + mountPath: /etc/kubernetes/provider +{{- end }} - name: openvpn-client image: quay.io/hypershift/openvpn:latest imagePullPolicy: Always @@ -160,3 +169,8 @@ spec: - name: apiserver-cm configMap: name: apiserver-default-audit-cm +{{- if .ProviderCredsSecretName }} + - name: provider-creds + secret: + secretName: {{ .ProviderCredsSecretName }} +{{- end }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/config.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/config.yaml index 426406bc4732..8cf5b481b2f4 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/config.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/config.yaml @@ -9,6 +9,8 @@ extendedArguments: - 'true' cert-dir: - "/var/run/kubernetes" + cloud-provider: + - {{ .CloudProvider }} cluster-cidr: - {{ .PodCIDR }} cluster-signing-cert-file: diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/kube-controller-manager-configmap.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/kube-controller-manager-configmap.yaml index e826d2440794..7be606b999fb 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/kube-controller-manager-configmap.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/kube-controller-manager-configmap.yaml @@ -7,3 +7,7 @@ data: {{ include_pki "combined-ca.crt" 4 }} service-ca.crt: |- {{ include_pki "combined-ca.crt" 4 }} +{{- if eq .CloudProvider "aws" }} + aws.conf: |- +{{ include "aws/aws.conf" 4 }} +{{- end}} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/kube-controller-manager-deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/kube-controller-manager-deployment.yaml index dc8b17547884..3c2c0fbaf046 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/kube-controller-manager-deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/kube-controller-manager/kube-controller-manager-deployment.yaml @@ -63,6 +63,13 @@ spec: containers: - name: kube-controller-manager image: {{ imageFor "hyperkube" }} + {{- if eq .CloudProvider "aws" }} + env: + - name: AWS_SHARED_CREDENTIALS_FILE + value: /etc/kubernetes/provider/credentials + - name: AWS_EC2_METADATA_DISABLED + value: "true" + {{- end }} command: - hyperkube - kube-controller-manager @@ -73,6 +80,10 @@ spec: - "--authorization-kubeconfig=/etc/kubernetes/secret/kubeconfig" - "--allocate-node-cidrs=true" - "--cert-dir=/var/run/kubernetes" +{{- if eq .CloudProvider "aws" }} + - "--cloud-config=/etc/kubernetes/config/aws.conf" +{{- end }} + - "--cloud-provider={{ .CloudProvider }}" - "--cluster-cidr={{ .PodCIDR }}" - "--cluster-signing-cert-file=/etc/kubernetes/secret/cluster-signer.crt" - "--cluster-signing-key-file=/etc/kubernetes/secret/cluster-signer.key" @@ -121,6 +132,10 @@ spec: name: certdir - mountPath: /var/log/kube-controller-manager name: logs +{{- if .ProviderCredsSecretName }} + - name: provider-creds + mountPath: /etc/kubernetes/provider +{{- end }} workingDir: /var/log/kube-controller-manager volumes: - secret: @@ -136,3 +151,8 @@ spec: name: logs - emptyDir: {} name: certdir +{{- if .ProviderCredsSecretName }} + - name: provider-creds + secret: + secretName: {{ .ProviderCredsSecretName }} +{{- end }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-dns-02-config.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-dns-02-config.yaml deleted file mode 100644 index 490d317d97ee..000000000000 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-dns-02-config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: config.openshift.io/v1 -kind: DNS -metadata: - name: cluster -spec: - baseDomain: {{ .BaseDomain }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-infrastructure-02-config.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-infrastructure-02-config.yaml deleted file mode 100644 index b9238f8d2474..000000000000 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-infrastructure-02-config.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: config.openshift.io/v1 -kind: Infrastructure -metadata: - name: cluster -spec: - cloudConfig: - name: "" -status: - apiServerInternalURI: https://{{ .ExternalAPIDNSName }}:{{ .ExternalAPIPort }} - apiServerURL: https://{{ .ExternalAPIDNSName }}:{{ .ExternalAPIPort }} - etcdDiscoveryDomain: {{ .BaseDomain }} - infrastructureName: kubernetes - platform: {{ if .PlatformType }}{{ .PlatformType }}{{ else }}None {{ end }} - platformStatus: {{ if .PlatformType }} - type: {{ .PlatformType }} {{ else }} - type: None {{ end }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-network-02-config.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-network-02-config.yaml deleted file mode 100644 index b108752a60e1..000000000000 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-network-02-config.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: config.openshift.io/v1 -kind: Network -metadata: - name: cluster -spec: - clusterNetwork: - - cidr: {{ .PodCIDR }} - hostPrefix: 23 - externalIP: - policy: {} - networkType: {{ .NetworkType }} - serviceNetwork: - - {{ .ServiceCIDR }} -status: {} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-proxy-01-config.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-proxy-01-config.yaml deleted file mode 100644 index 475e4afaedde..000000000000 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/cluster-proxy-01-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: config.openshift.io/v1 -kind: Proxy -metadata: - name: cluster -spec: - trustedCA: - name: "" -status: {} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/install-config.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/install-config.yaml deleted file mode 100644 index db711bb79a0d..000000000000 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/install-config.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -controlPlane: - replicas: 1 -networking: - machineCIDR: 10.0.0.0/16 -platform: - none: {} diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/machine-config-server-configmap.yaml b/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/machine-config-server-configmap.yaml index 76a9d54a4f5b..b51da32f5d4c 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/machine-config-server-configmap.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/machine-config-server/machine-config-server-configmap.yaml @@ -8,15 +8,15 @@ data: combined-ca.crt: |- {{ include_pki "combined-ca.crt" 4 }} cluster-dns-02-config.yaml: |- -{{ include "machine-config-server/cluster-dns-02-config.yaml" 4 }} +{{ include "cluster-bootstrap/cluster-dns-02-config.yaml" 4 }} cluster-infrastructure-02-config.yaml: |- -{{ include "machine-config-server/cluster-infrastructure-02-config.yaml" 4 }} +{{ include "cluster-bootstrap/cluster-infrastructure-02-config.yaml" 4 }} cluster-network-02-config.yaml: |- -{{ include "machine-config-server/cluster-network-02-config.yaml" 4 }} +{{ include "cluster-bootstrap/cluster-network-02-config.yaml" 4 }} cluster-proxy-01-config.yaml: |- -{{ include "machine-config-server/cluster-proxy-01-config.yaml" 4 }} +{{ include "cluster-bootstrap/cluster-proxy-01-config.yaml" 4 }} install-config.yaml: |- -{{ include "machine-config-server/install-config.yaml" 4 }} +{{ include "install-config/install-config.yaml" 4 }} pull-secret.yaml: |- {{ include "machine-config-server/pull-secret.yaml" 4 }} master.machineconfigpool.yaml: |- diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/openvpn/server.conf b/control-plane-operator/controllers/hostedcontrolplane/assets/openvpn/server.conf index 1526023e55b7..59bb4e5fac8e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/openvpn/server.conf +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/openvpn/server.conf @@ -24,6 +24,7 @@ client-config-dir /etc/openvpn/ccd ### Route Configurations Below route {{ address .PodCIDR }} {{ mask .PodCIDR }} route {{ address .ServiceCIDR }} {{ mask .ServiceCIDR }} +route {{ address .MachineCIDR }} {{ mask .MachineCIDR }} ### Push Configurations Below @@ -34,3 +35,4 @@ duplicate-cn client-to-client push "route {{ address .PodCIDR }} {{ mask .PodCIDR }}" push "route {{ address .ServiceCIDR }} {{ mask .ServiceCIDR }}" +push "route {{ address .MachineCIDR }} {{ mask .MachineCIDR }}" diff --git a/control-plane-operator/controllers/hostedcontrolplane/assets/openvpn/worker b/control-plane-operator/controllers/hostedcontrolplane/assets/openvpn/worker index f8561aeaeaa7..e7e2b6bc8918 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/assets/openvpn/worker +++ b/control-plane-operator/controllers/hostedcontrolplane/assets/openvpn/worker @@ -1,2 +1,3 @@ iroute {{ address .ServiceCIDR }} {{ mask .ServiceCIDR }} iroute {{ address .PodCIDR }} {{ mask .PodCIDR }} +iroute {{ address .MachineCIDR }} {{ mask .MachineCIDR }} diff --git a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go index ee7072eaa6bd..7c5ecc2db1e4 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go +++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go @@ -519,10 +519,10 @@ func (r *HostedControlPlaneReconciler) generateControlPlaneManifests(ctx context if err != nil { return nil, fmt.Errorf("couldn't determine cluster base domain name: %w", err) } - - var clusterInfra configv1.Infrastructure - if err := r.Get(context.Background(), client.ObjectKey{Name: "cluster"}, &clusterInfra); err != nil { - return nil, fmt.Errorf("failed to get cluster infra: %w", err) + var cloudCreds corev1.Secret + err = r.Client.Get(ctx, client.ObjectKey{Namespace: hcp.Namespace, Name: hcp.Spec.ProviderCreds.Name}, &cloudCreds) + if err != nil { + return nil, fmt.Errorf("failed to get provider credentials secret %s: %w", hcp.Spec.ProviderCreds.Name, err) } params := render.NewClusterParams() @@ -536,14 +536,28 @@ func (r *HostedControlPlaneReconciler) generateControlPlaneManifests(ctx context params.ExternalOauthPort = externalOauthPort params.ServiceCIDR = hcp.Spec.ServiceCIDR params.PodCIDR = hcp.Spec.PodCIDR + params.MachineCIDR = hcp.Spec.MachineCIDR params.ReleaseImage = hcp.Spec.ReleaseImage params.IngressSubdomain = fmt.Sprintf("apps.%s", baseDomain) params.OpenShiftAPIClusterIP = infraStatus.OpenShiftAPIAddress params.OauthAPIClusterIP = infraStatus.OauthAPIServerAddress params.BaseDomain = baseDomain params.MachineConfigServerAddress = infraStatus.IgnitionProviderAddress - params.CloudProvider = string(clusterInfra.Status.PlatformStatus.Type) - params.PlatformType = string(clusterInfra.Status.PlatformStatus.Type) + params.CloudProvider = cloudProvider(hcp) + params.PlatformType = platformType(hcp) + params.InfraID = hcp.Spec.InfraID + if hcp.Spec.Platform.AWS != nil { + params.AWSRegion = hcp.Spec.Platform.AWS.Region + params.AWSVPCID = hcp.Spec.Platform.AWS.VPC + if hcp.Spec.Platform.AWS.NodePoolDefaults != nil { + params.AWSZone = hcp.Spec.Platform.AWS.NodePoolDefaults.Zone + if hcp.Spec.Platform.AWS.NodePoolDefaults.Subnet.ID != nil { + params.AWSSubnetID = *hcp.Spec.Platform.AWS.NodePoolDefaults.Subnet.ID + } + } + } + params.CloudCredentials = string(cloudCreds.Data["credentials"]) + params.ProviderCredsSecretName = hcp.Spec.ProviderCreds.Name params.InternalAPIPort = APIServerPort params.EtcdClientName = "etcd-client" params.NetworkType = "OpenShiftSDN" @@ -551,7 +565,6 @@ func (r *HostedControlPlaneReconciler) generateControlPlaneManifests(ctx context params.APIAvailabilityPolicy = render.SingleReplica params.ControllerAvailabilityPolicy = render.SingleReplica params.SSHKey = string(sshKeyData) - params.HypershiftOperatorControllers = []string{"route-sync", "auto-approver", "kubeadmin-password", "node"} // Generate PKI data just once and store it in a secret. PKI generation isn't // deterministic and shouldn't be performed with every reconcile, otherwise @@ -618,27 +631,40 @@ func (r *HostedControlPlaneReconciler) generateControlPlaneManifests(ctx context return nil, fmt.Errorf("failed to render hypershift manifests for cluster: %w", err) } - kubeAPIServerContext := render.NewKubeAPIServerManifestContext(&render.KubeAPIServerParams{ - PodCIDR: params.PodCIDR, - ServiceCIDR: params.ServiceCIDR, - ExternalAPIAddress: params.ExternalAPIAddress, - APIServerAuditEnabled: params.APIServerAuditEnabled, - CloudProvider: params.CloudProvider, - EtcdClientName: params.EtcdClientName, - DefaultFeatureGates: params.DefaultFeatureGates, - ExtraFeatureGates: params.ExtraFeatureGates, - IngressSubdomain: params.IngressSubdomain, - InternalAPIPort: params.InternalAPIPort, - NamedCerts: params.NamedCerts, - PKI: pkiSecret.Data, - APIAvailabilityPolicy: render.KubeAPIServerParamsAvailabilityPolicy(params.APIAvailabilityPolicy), - ClusterID: params.ClusterID, - Images: releaseImage.ComponentImages(), - ApiserverLivenessPath: params.ApiserverLivenessPath, - APINodePort: params.APINodePort, - ExternalOauthPort: params.ExternalOauthPort, - ExternalOauthDNSName: params.ExternalOauthDNSName, - }) + kubeAPIServerParams := &render.KubeAPIServerParams{ + PodCIDR: params.PodCIDR, + ServiceCIDR: params.ServiceCIDR, + ExternalAPIAddress: params.ExternalAPIAddress, + APIServerAuditEnabled: params.APIServerAuditEnabled, + CloudProvider: params.CloudProvider, + EtcdClientName: params.EtcdClientName, + DefaultFeatureGates: params.DefaultFeatureGates, + ExtraFeatureGates: params.ExtraFeatureGates, + IngressSubdomain: params.IngressSubdomain, + InternalAPIPort: params.InternalAPIPort, + NamedCerts: params.NamedCerts, + PKI: pkiSecret.Data, + APIAvailabilityPolicy: render.KubeAPIServerParamsAvailabilityPolicy(params.APIAvailabilityPolicy), + ClusterID: params.ClusterID, + Images: releaseImage.ComponentImages(), + ApiserverLivenessPath: params.ApiserverLivenessPath, + APINodePort: params.APINodePort, + ExternalOauthPort: params.ExternalOauthPort, + ExternalOauthDNSName: params.ExternalOauthDNSName, + ProviderCredsSecretName: hcp.Spec.ProviderCreds.Name, + InfraID: hcp.Spec.InfraID, + } + if hcp.Spec.Platform.AWS != nil { + kubeAPIServerParams.AWSRegion = hcp.Spec.Platform.AWS.Region + kubeAPIServerParams.AWSVPCID = hcp.Spec.Platform.AWS.VPC + if hcp.Spec.Platform.AWS.NodePoolDefaults != nil { + kubeAPIServerParams.AWSZone = hcp.Spec.Platform.AWS.NodePoolDefaults.Zone + if hcp.Spec.Platform.AWS.NodePoolDefaults.Subnet.ID != nil { + kubeAPIServerParams.AWSSubnetID = *hcp.Spec.Platform.AWS.NodePoolDefaults.Subnet.ID + } + } + } + kubeAPIServerContext := render.NewKubeAPIServerManifestContext(kubeAPIServerParams) kubeAPIServerManifests, err := kubeAPIServerContext.Render() if err != nil { return nil, fmt.Errorf("failed to render kube apiserver manifests: %w", err) @@ -1071,3 +1097,21 @@ func generateImageRegistrySecret() string { rand.Read(num) return hex.EncodeToString(num) } + +func platformType(hcp *hyperv1.HostedControlPlane) string { + switch { + case hcp.Spec.Platform.AWS != nil: + return "AWS" + default: + return "None" + } +} + +func cloudProvider(hcp *hyperv1.HostedControlPlane) string { + switch { + case hcp.Spec.Platform.AWS != nil: + return "aws" + default: + return "" + } +} diff --git a/control-plane-operator/controllers/hostedcontrolplane/render/funcs.go b/control-plane-operator/controllers/hostedcontrolplane/render/funcs.go index 3ebeef5845c8..dc95393c3cd7 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/render/funcs.go +++ b/control-plane-operator/controllers/hostedcontrolplane/render/funcs.go @@ -12,6 +12,7 @@ import ( "github.com/blang/semver" "github.com/vincent-petithory/dataurl" + "gopkg.in/ini.v1" ) func includeVPNFunc(includeVPN bool) func() bool { @@ -152,6 +153,22 @@ func cidrMask(cidr string) string { return fmt.Sprintf("%d.%d.%d.%d", m[0], m[1], m[2], m[3]) } +func iniValue(iniContent, section, key string) string { + f, err := ini.Load([]byte(iniContent)) + if err != nil { + panic(err.Error()) + } + s, err := f.GetSection(section) + if err != nil { + panic(err.Error()) + } + k, err := s.GetKey(key) + if err != nil { + panic(err.Error()) + } + return k.String() +} + // randomString uses RawURLEncoding to ensure we do not get / characters or trailing ='s func randomString(size int) string { // each byte (8 bits) gives us 4/3 base64 (6 bits) characters diff --git a/control-plane-operator/controllers/hostedcontrolplane/render/kube_apiserver.go b/control-plane-operator/controllers/hostedcontrolplane/render/kube_apiserver.go index 02e440ff5f58..ce61658a0354 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/render/kube_apiserver.go +++ b/control-plane-operator/controllers/hostedcontrolplane/render/kube_apiserver.go @@ -3,25 +3,31 @@ package render import "text/template" type KubeAPIServerParams struct { - PodCIDR string - ServiceCIDR string - ExternalAPIAddress string - APIServerAuditEnabled bool - CloudProvider string - EtcdClientName string - DefaultFeatureGates []string - ExtraFeatureGates []string - IngressSubdomain string - InternalAPIPort uint - NamedCerts []NamedCert - PKI map[string][]byte - APIAvailabilityPolicy KubeAPIServerParamsAvailabilityPolicy - ClusterID string - Images map[string]string - ApiserverLivenessPath string - APINodePort uint - ExternalOauthPort uint - ExternalOauthDNSName string + PodCIDR string + ServiceCIDR string + ExternalAPIAddress string + APIServerAuditEnabled bool + CloudProvider string + EtcdClientName string + DefaultFeatureGates []string + ExtraFeatureGates []string + InfraID string + IngressSubdomain string + InternalAPIPort uint + NamedCerts []NamedCert + PKI map[string][]byte + APIAvailabilityPolicy KubeAPIServerParamsAvailabilityPolicy + ClusterID string + Images map[string]string + ApiserverLivenessPath string + APINodePort uint + ExternalOauthPort uint + ExternalOauthDNSName string + ProviderCredsSecretName string + AWSZone string + AWSVPCID string + AWSRegion string + AWSSubnetID string } type KubeAPIServerParamsAvailabilityPolicy string diff --git a/control-plane-operator/controllers/hostedcontrolplane/render/manifests.go b/control-plane-operator/controllers/hostedcontrolplane/render/manifests.go index 0c7d0af465a2..3480febf522b 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/render/manifests.go +++ b/control-plane-operator/controllers/hostedcontrolplane/render/manifests.go @@ -50,6 +50,7 @@ func newClusterManifestContext(images, versions map[string]string, params interf "pullSecretBase64": pullSecretBase64(pullSecret), "atleast_version": atLeastVersionFunc(versions), "lessthan_version": lessThanVersionFunc(versions), + "ini_value": iniValue, }) return ctx } diff --git a/control-plane-operator/controllers/hostedcontrolplane/render/types.go b/control-plane-operator/controllers/hostedcontrolplane/render/types.go index 3baa1cc5d77e..b45bb4571da7 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/render/types.go +++ b/control-plane-operator/controllers/hostedcontrolplane/render/types.go @@ -49,6 +49,7 @@ type ClusterParams struct { ExternalOauthPort uint `json:"externalOauthPort"` IdentityProviders string `json:"identityProviders"` ServiceCIDR string `json:"serviceCIDR"` + MachineCIDR string `json:"machineCIDR"` NamedCerts []NamedCert `json:"namedCerts,omitempty"` PodCIDR string `json:"podCIDR"` ReleaseImage string `json:"releaseImage"` @@ -97,8 +98,17 @@ type ClusterParams struct { HypershiftOperatorControllers []string `json:"hypershiftOperatorControllers"` MachineConfigServerAddress string `json:"machineConfigServerAddress"` SSHKey string `json:"sshKey"` + InfraID string `json:"infraID"` + ProviderCredsSecretName string `json:"providerCredsSecretName"` + CloudCredentials string `json:"cloudCredentials"` DefaultFeatureGates []string + // AWS params + AWSZone string `json:"awsZone"` + AWSVPCID string `json:"awsVPCID"` + AWSRegion string `json:"awsRegion"` + AWSSubnetID string `json:"awsSubnetID"` + // Fields below are are taken from the ROKs type EndpointPublishingStrategyScope string `json:"endpointPublishingStrategyScope"` ClusterID string `json:"clusterID"` diff --git a/docs/api.md b/docs/api.md index e1f82abf6e87..0d41ec82e573 100644 --- a/docs/api.md +++ b/docs/api.md @@ -59,9 +59,6 @@ type AWSPlatformSpec struct { // Region is the AWS region for the cluster Region string - // AvailabilityZone is the default availability zone for the cluster - AvailabilityZone string - // VPC specifies the VPC used for the cluster VPC string `json:"vpc"` @@ -247,11 +244,12 @@ type NodePoolPlatform struct { type AWSNodePoolPlatform struct { // InstanceType defines the ec2 instance type. // eg. m4-large - InstanceType string `json:"instanceType"` - InstanceProfile string `json:"instanceProfile,omitempty"` - Subnet *AWSResourceReference `json:"subnet,omitempty"` - SecurityGroups []string `json:"securityGroups,omitempty"` - AMI string `json:"ami"` + Zone string `json:"zone"` + InstanceType string `json:"instanceType"` + InstanceProfile string `json:"instanceProfile,omitempty"` + Subnet *AWSResourceReference `json:"subnet,omitempty"` + SecurityGroups []AWSResourceReference `json:"securityGroups,omitempty"` + AMI string `json:"ami"` } // AWSResourceReference is a reference to a specific AWS resource by ID, ARN, or filters. diff --git a/go.mod b/go.mod index cc5412e29b00..5b8ee705f4ac 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/stretchr/testify v1.6.1 github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 + gopkg.in/ini.v1 v1.51.0 k8s.io/api v0.20.2 k8s.io/apiextensions-apiserver v0.20.2 k8s.io/apimachinery v0.20.2 diff --git a/go.sum b/go.sum index f051c966ac3a..2f2dd5d8e8c9 100644 --- a/go.sum +++ b/go.sum @@ -207,6 +207,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1 h1:A8Yhf6EtqTv9RMsU6MQTyrtV1TjWlR6xU9BsZIwuTCM= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -256,6 +257,7 @@ github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -378,7 +380,9 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -695,6 +699,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go index d55ea868351d..2f721658691c 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go @@ -1,6 +1,4 @@ /* - - 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 @@ -271,12 +269,6 @@ func (r *HostedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reques } r.Log.Info("Created ssh key secret in the target namespace", "namespace", targetNamespace) - var infra configv1.Infrastructure - if err := r.Get(context.Background(), client.ObjectKey{Name: "cluster"}, &infra); err != nil { - r.Log.Error(err, "failed to get cluster infra") - return ctrl.Result{}, fmt.Errorf("failed to get cluster infra: %w", err) - } - controlPlaneOperatorServiceAccount := controlplaneoperator.OperatorServiceAccount{Namespace: targetNamespace}.Build() controlPlaneOperatorClusterRole := controlplaneoperator.OperatorClusterRole{}.Build() controlPlaneOperatorClusterRoleBinding := controlplaneoperator.OperatorClusterRoleBinding{ @@ -327,7 +319,6 @@ func (r *HostedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reques eic := controlplaneoperator.ExternalInfraCluster{ Namespace: targetNamespace, HostedCluster: hcluster, - InfraConfig: &infra, }.Build() createOnlyObjects := []ctrlclient.Object{ capiCluster, diff --git a/hypershift-operator/controllers/hostedcluster/manifests/controlplaneoperator/manifests.go b/hypershift-operator/controllers/hostedcluster/manifests/controlplaneoperator/manifests.go index dcb32f5f1a20..87be0e7675c7 100644 --- a/hypershift-operator/controllers/hostedcluster/manifests/controlplaneoperator/manifests.go +++ b/hypershift-operator/controllers/hostedcluster/manifests/controlplaneoperator/manifests.go @@ -1,7 +1,6 @@ package controlplaneoperator import ( - configv1 "github.com/openshift/api/config/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -336,9 +335,12 @@ func (o HostedControlPlane) Build() *hyperv1.HostedControlPlane { SSHKey: corev1.LocalObjectReference{ Name: o.SSHKey.Name, }, - ServiceCIDR: o.HostedCluster.Spec.ServiceCIDR, - PodCIDR: o.HostedCluster.Spec.PodCIDR, + ServiceCIDR: o.HostedCluster.Spec.Networking.ServiceCIDR, + PodCIDR: o.HostedCluster.Spec.Networking.PodCIDR, + MachineCIDR: o.HostedCluster.Spec.Networking.MachineCIDR, ReleaseImage: o.HostedCluster.Spec.Release.Image, + InfraID: o.HostedCluster.Spec.InfraID, + Platform: o.HostedCluster.Spec.Platform, }, } return hcp @@ -347,7 +349,6 @@ func (o HostedControlPlane) Build() *hyperv1.HostedControlPlane { type ExternalInfraCluster struct { Namespace *corev1.Namespace HostedCluster *hyperv1.HostedCluster - InfraConfig *configv1.Infrastructure } func (o ExternalInfraCluster) Build() *hyperv1.ExternalInfraCluster { @@ -365,8 +366,10 @@ func (o ExternalInfraCluster) Build() *hyperv1.ExternalInfraCluster { }, Spec: hyperv1.ExternalInfraClusterSpec{ ComputeReplicas: o.HostedCluster.Spec.InitialComputeReplicas, - Region: o.InfraConfig.Status.PlatformStatus.AWS.Region, }, } + if o.HostedCluster.Spec.Platform.AWS != nil { + eic.Spec.Region = o.HostedCluster.Spec.Platform.AWS.Region + } return eic } diff --git a/hypershift-operator/controllers/hostedcluster/manifests/manifests.go b/hypershift-operator/controllers/hostedcluster/manifests/manifests.go index 125ea622ca64..6a6bdac55a00 100644 --- a/hypershift-operator/controllers/hostedcluster/manifests/manifests.go +++ b/hypershift-operator/controllers/hostedcluster/manifests/manifests.go @@ -105,14 +105,12 @@ func (o DefaultNodePool) Build() *hyperv1.NodePool { Spec: hyperv1.NodePoolSpec{ ClusterName: o.HostedCluster.GetName(), NodeCount: k8sutilspointer.Int32Ptr(int32(o.HostedCluster.Spec.InitialComputeReplicas)), - Platform: hyperv1.NodePoolPlatform{ - AWS: &hyperv1.AWSNodePoolPlatform{ - InstanceType: "m5.large", - }, - }, }, Status: hyperv1.NodePoolStatus{}, } + if o.HostedCluster.Spec.Platform.AWS != nil { + nodePool.Spec.Platform.AWS = o.HostedCluster.Spec.Platform.AWS.NodePoolDefaults + } return nodePool } diff --git a/hypershift-operator/controllers/machineimage/interface.go b/hypershift-operator/controllers/machineimage/interface.go new file mode 100644 index 000000000000..07649b6c0085 --- /dev/null +++ b/hypershift-operator/controllers/machineimage/interface.go @@ -0,0 +1,8 @@ +package machineimage + +import hyperv1 "github.com/openshift/hypershift/api/v1alpha1" + +// ImageProvider provides a cloud image to use for a given HostedCluster +type Provider interface { + Image(cluster *hyperv1.HostedCluster) (string, error) +} diff --git a/hypershift-operator/controllers/machineimage/static/4.7/rhcos-amd64.json b/hypershift-operator/controllers/machineimage/static/4.7/rhcos-amd64.json new file mode 100644 index 000000000000..a7c191006a49 --- /dev/null +++ b/hypershift-operator/controllers/machineimage/static/4.7/rhcos-amd64.json @@ -0,0 +1,163 @@ +{ + "amis": { + "af-south-1": { + "hvm": "ami-057e5df70c52dc128" + }, + "ap-east-1": { + "hvm": "ami-006ab68917f52bb13" + }, + "ap-northeast-1": { + "hvm": "ami-0d236f6289c700771" + }, + "ap-northeast-2": { + "hvm": "ami-040394572427a293a" + }, + "ap-south-1": { + "hvm": "ami-0838c978c0390dd75" + }, + "ap-southeast-1": { + "hvm": "ami-07af688c8b65de56f" + }, + "ap-southeast-2": { + "hvm": "ami-0a36faab6aa0a0dea" + }, + "ca-central-1": { + "hvm": "ami-01284e5815ce66a95" + }, + "eu-central-1": { + "hvm": "ami-0361c06cf3e935cfe" + }, + "eu-north-1": { + "hvm": "ami-0080eb90a48d9655e" + }, + "eu-south-1": { + "hvm": "ami-0a3bc89f7aadf0343" + }, + "eu-west-1": { + "hvm": "ami-0b4024fa5cb2588bd" + }, + "eu-west-2": { + "hvm": "ami-07376355104ab4106" + }, + "eu-west-3": { + "hvm": "ami-038f4ce9ea7ac7191" + }, + "me-south-1": { + "hvm": "ami-025899013a24bb708" + }, + "sa-east-1": { + "hvm": "ami-089e1a3dcc5a5fe08" + }, + "us-east-1": { + "hvm": "ami-0d5f9982f029fbc14" + }, + "us-east-2": { + "hvm": "ami-0c84b5c5255ec4777" + }, + "us-west-1": { + "hvm": "ami-0b421328859954025" + }, + "us-west-2": { + "hvm": "ami-010de485a2ee23e5e" + } + }, + "azure": { + "image": "rhcos-47.83.202102090044-0-azure.x86_64.vhd", + "url": "https://rhcos.blob.core.windows.net/imagebucket/rhcos-47.83.202102090044-0-azure.x86_64.vhd" + }, + "baseURI": "https://releases-art-rhcos.svc.ci.openshift.org/art/storage/releases/rhcos-4.7/47.83.202102090044-0/x86_64/", + "buildid": "47.83.202102090044-0", + "gcp": { + "image": "rhcos-47-83-202102090044-0-gcp-x86-64", + "project": "rhcos-cloud", + "url": "https://storage.googleapis.com/rhcos/rhcos/rhcos-47-83-202102090044-0-gcp-x86-64.tar.gz" + }, + "images": { + "aws": { + "path": "rhcos-47.83.202102090044-0-aws.x86_64.vmdk.gz", + "sha256": "ad54945302d9aaf0c5d6a5d1ee457325a3dcd88a68067d18a4d614acef5390d4", + "size": 951239242, + "uncompressed-sha256": "fbb7fbbc6cc6161ffa5c571f1b9022fe80038eea0a7f099c5b18ceba3b82f5eb", + "uncompressed-size": 970902016 + }, + "azure": { + "path": "rhcos-47.83.202102090044-0-azure.x86_64.vhd.gz", + "sha256": "32aa9be04b7f06bfe028fc7c93443f0c742afb006e73b6076016fa897ae5f716", + "size": 951756228, + "uncompressed-sha256": "7e60c02aeebd129a99d65da48beddb99bd29ac7b31f3bb9395fb1d87e6311448", + "uncompressed-size": 17179869696 + }, + "gcp": { + "path": "rhcos-47.83.202102090044-0-gcp.x86_64.tar.gz", + "sha256": "07275bf2844f0dba6296d984d293959f614cc60af9f1be4070142a8cdcc650aa", + "size": 937047583 + }, + "ibmcloud": { + "path": "rhcos-47.83.202102090044-0-ibmcloud.x86_64.qcow2.gz", + "sha256": "b81b26a61d11a9c2c6fb33a94ff124f461907bff550338cfecf173cdd93c09be", + "size": 937381841, + "uncompressed-sha256": "5b6fdfb53e3ede4d264d13862bda6801d932f12e1a17309ac62ac32836bca8ff", + "uncompressed-size": 2360082432 + }, + "live-initramfs": { + "path": "rhcos-47.83.202102090044-0-live-initramfs.x86_64.img", + "sha256": "29ba7e12b16f143a3f036226568ce91b3f7c8bd6feab719a73b0bed1817e4444" + }, + "live-iso": { + "path": "rhcos-47.83.202102090044-0-live.x86_64.iso", + "sha256": "b765a9e99edfa0a91778a4787070c6afcaa198ba8806c8c1533c34a23d63136f" + }, + "live-kernel": { + "path": "rhcos-47.83.202102090044-0-live-kernel-x86_64", + "sha256": "7da6617f6bb7b29c0a8cba002642f2fa23d954874e0d3641e61dd59573563381" + }, + "live-rootfs": { + "path": "rhcos-47.83.202102090044-0-live-rootfs.x86_64.img", + "sha256": "6da4ae110fc2bcea8ac0e6c40fa9d701bbde5b936a1410f75735386c8ea805bf" + }, + "metal": { + "path": "rhcos-47.83.202102090044-0-metal.x86_64.raw.gz", + "sha256": "0a456b02960eeb40a102ef14f5556843c2c504358f3db21f3fe667d8346c9bdf", + "size": 939085053, + "uncompressed-sha256": "36379d083adccccb3de266d2802cfcf5079429865fa010d9b499e87042d46526", + "uncompressed-size": 3717201920 + }, + "metal4k": { + "path": "rhcos-47.83.202102090044-0-metal4k.x86_64.raw.gz", + "sha256": "ff10909c5d4ffbb3a829d070dcf3ce9a8b2d2aebff2924ee464d517eb948bfba", + "size": 936609970, + "uncompressed-sha256": "d5821d7fbd20f86bab29d028855200221f316fa041e86374c5f693da9b5d7b83", + "uncompressed-size": 3717201920 + }, + "openstack": { + "path": "rhcos-47.83.202102090044-0-openstack.x86_64.qcow2.gz", + "sha256": "e5ebb8bcf6d081e52e1e7db0e93ff960ff472e25803a5671170959212e88cad9", + "size": 937380970, + "uncompressed-sha256": "c1b93a426d0f74f0059193439e306f3356b788302a825cfadd870460e543028e", + "uncompressed-size": 2360082432 + }, + "ostree": { + "path": "rhcos-47.83.202102090044-0-ostree.x86_64.tar", + "sha256": "3b5fc040f7493ff7dc5aef4da22077ad74eb2e12a01a9e820d5c116e58716c4f", + "size": 863467520 + }, + "qemu": { + "path": "rhcos-47.83.202102090044-0-qemu.x86_64.qcow2.gz", + "sha256": "2ca82f1d762bba1cb2e5ac1386be0a1ab0264cf88b0594c9cd1e53d285833c6d", + "size": 938521497, + "uncompressed-sha256": "5d31652c7856a87450dce1bbbb561b578ee75443c190096cb977a814e5f35935", + "uncompressed-size": 2394030080 + }, + "vmware": { + "path": "rhcos-47.83.202102090044-0-vmware.x86_64.ova", + "sha256": "13d92692b8eed717ff8d0d113a24add339a65ef1f12eceeb99dabcd922cc86d1", + "size": 970915840 + } + }, + "oscontainer": { + "digest": "sha256:a32077727aa2ef96a1e2371dbcc53ba06f3d9727e836b72be0f0dd4513937e1e", + "image": "quay.io/openshift-release-dev/ocp-v4.0-art-dev" + }, + "ostree-commit": "646a9832dd0dc9fe174a2fc005863a9582186518a5476522a0e9bdccc0e5252a", + "ostree-version": "47.83.202102090044-0" +} \ No newline at end of file diff --git a/hypershift-operator/controllers/machineimage/static/assets.go b/hypershift-operator/controllers/machineimage/static/assets.go new file mode 100644 index 000000000000..e8fa9f2820ba --- /dev/null +++ b/hypershift-operator/controllers/machineimage/static/assets.go @@ -0,0 +1,15 @@ +package static + +import "embed" + +//go:embed 4.7/* + +var content embed.FS + +func MustAsset(name string) []byte { + b, err := content.ReadFile(name) + if err != nil { + panic(err) + } + return b +} diff --git a/hypershift-operator/controllers/machineimage/static/provider.go b/hypershift-operator/controllers/machineimage/static/provider.go new file mode 100644 index 000000000000..597e2b3589ba --- /dev/null +++ b/hypershift-operator/controllers/machineimage/static/provider.go @@ -0,0 +1,35 @@ +package static + +import ( + "encoding/json" + "fmt" + + hyperv1 "github.com/openshift/hypershift/api/v1alpha1" + "github.com/openshift/hypershift/hypershift-operator/controllers/machineimage" +) + +type StaticImageProvider struct { +} + +var _ machineimage.Provider = &StaticImageProvider{} + +type regionImage struct { + HVMImage string `json:"hvm"` +} + +type staticImages struct { + AMIs map[string]regionImage `json:"amis"` +} + +func (p *StaticImageProvider) Image(cluster *hyperv1.HostedCluster) (string, error) { + if cluster.Spec.Platform.AWS == nil { + return "", fmt.Errorf("unsupported platform, only AWS is supported") + } + // TODO: Support other versions, other archs. Currently only 4.7 amd64 is supported. + imageData := MustAsset("4.7/rhcos-amd64.json") + images := &staticImages{} + if err := json.Unmarshal(imageData, images); err != nil { + return "", fmt.Errorf("cannot decode image data: %w", err) + } + return images.AMIs[cluster.Spec.Platform.AWS.Region].HVMImage, nil +} diff --git a/hypershift-operator/controllers/machineimage/static/provider_test.go b/hypershift-operator/controllers/machineimage/static/provider_test.go new file mode 100644 index 000000000000..b47b6e460bbb --- /dev/null +++ b/hypershift-operator/controllers/machineimage/static/provider_test.go @@ -0,0 +1,27 @@ +package static + +import ( + "testing" + + hyperv1 "github.com/openshift/hypershift/api/v1alpha1" +) + +func TestImage(t *testing.T) { + hc := &hyperv1.HostedCluster{ + Spec: hyperv1.HostedClusterSpec{ + Platform: hyperv1.PlatformSpec{ + AWS: &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + }, + }, + }, + } + p := &StaticImageProvider{} + image, err := p.Image(hc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if image != "ami-0d5f9982f029fbc14" { + t.Fatalf("unexpected image: %s", image) + } +} diff --git a/hypershift-operator/controllers/nodepool/nodepool_controller.go b/hypershift-operator/controllers/nodepool/nodepool_controller.go index 545f6b1e3c7c..e9943dc36c2d 100644 --- a/hypershift-operator/controllers/nodepool/nodepool_controller.go +++ b/hypershift-operator/controllers/nodepool/nodepool_controller.go @@ -10,14 +10,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "github.com/go-logr/logr" - configv1 "github.com/openshift/api/config/v1" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" @@ -32,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" hyperv1 "github.com/openshift/hypershift/api/v1alpha1" + "github.com/openshift/hypershift/hypershift-operator/controllers/machineimage" hyperutil "github.com/openshift/hypershift/hypershift-operator/controllers/util" capiv1 "github.com/openshift/hypershift/thirdparty/clusterapi/api/v1alpha4" "github.com/openshift/hypershift/thirdparty/clusterapi/util" @@ -48,8 +46,9 @@ const ( type NodePoolReconciler struct { ctrlclient.Client - recorder record.EventRecorder - Log logr.Logger + recorder record.EventRecorder + Log logr.Logger + ImageProvider machineimage.Provider } func (r *NodePoolReconciler) SetupWithManager(mgr ctrl.Manager) error { @@ -90,16 +89,16 @@ func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c if err != nil { return ctrl.Result{}, err } - var infra configv1.Infrastructure - if err := r.Get(context.Background(), client.ObjectKey{Name: "cluster"}, &infra); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to get cluster infra: %w", err) - } targetNamespace := hcluster.GetName() // Ignore deleted nodePools, this can happen when foregroundDeletion // is enabled if !nodePool.DeletionTimestamp.IsZero() { - machineSet, _, err := generateScalableResources(r, ctx, infra.Status.InfrastructureName, infra.Status.PlatformStatus.AWS.Region, nodePool, targetNamespace) + ami, err := r.ImageProvider.Image(hcluster) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to obtain AMI: %w", err) + } + machineSet, _, err := generateScalableResources(r, ctx, hcluster.Spec.InfraID, hcluster.Spec.Platform.AWS.Region, ami, nodePool, targetNamespace) if err != nil { return reconcile.Result{}, fmt.Errorf("failed to generate worker machineset: %w", err) } @@ -131,7 +130,7 @@ func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, err } - result, err := r.reconcile(ctx, hcluster, &infra, nodePool) + result, err := r.reconcile(ctx, hcluster, nodePool) if err != nil { r.Log.Error(err, "Failed to reconcile nodePool") r.recorder.Eventf(nodePool, corev1.EventTypeWarning, "ReconcileError", "%v", err) @@ -151,7 +150,7 @@ func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return result, nil } -func (r *NodePoolReconciler) reconcile(ctx context.Context, hcluster *hyperv1.HostedCluster, infra *configv1.Infrastructure, nodePool *hyperv1.NodePool) (ctrl.Result, error) { +func (r *NodePoolReconciler) reconcile(ctx context.Context, hcluster *hyperv1.HostedCluster, nodePool *hyperv1.NodePool) (ctrl.Result, error) { log := ctrl.LoggerFrom(ctx) log.Info("Reconcile nodePool") @@ -175,9 +174,14 @@ func (r *NodePoolReconciler) reconcile(ctx context.Context, hcluster *hyperv1.Ho // Generate scalable resource for nodePool targetNamespace := hcluster.GetName() + ami, err := r.ImageProvider.Image(hcluster) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to obtain AMI: %w", err) + } scalableResource, AWSMachineTemplate, err := generateScalableResources(r, ctx, - infra.Status.InfrastructureName, - infra.Status.PlatformStatus.AWS.Region, + hcluster.Spec.InfraID, + hcluster.Spec.Platform.AWS.Region, + ami, nodePool, targetNamespace) if err != nil { @@ -269,28 +273,7 @@ func GetHostedClusterByName(ctx context.Context, c client.Client, namespace, nam } func generateScalableResources(client ctrlclient.Client, ctx context.Context, - infraName, region string, nodePool *hyperv1.NodePool, targetNamespace string) (*capiv1.MachineDeployment, *capiaws.AWSMachineTemplate, error) { - // find AMI - machineSets := &unstructured.UnstructuredList{} - machineSets.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "machine.openshift.io", - Version: "v1beta1", - Kind: "MachineSet", - }) - if err := client.List(ctx, machineSets, ctrlclient.InNamespace("openshift-machine-api")); err != nil { - return nil, nil, fmt.Errorf("failed to list machinesets: %w", err) - } - if len(machineSets.Items) == 0 { - return nil, nil, fmt.Errorf("no machinesets found") - } - obj := machineSets.Items[0] - object := obj.Object - - AMI, found, err := unstructured.NestedString(object, "spec", "template", "spec", "providerSpec", "value", "ami", "id") - if err != nil || !found { - return nil, nil, fmt.Errorf("error finding AMI. Found: %v. Error: %v", found, err) - } - + infraName, region, ami string, nodePool *hyperv1.NodePool, targetNamespace string) (*capiv1.MachineDeployment, *capiaws.AWSMachineTemplate, error) { subnet := &capiaws.AWSResourceReference{} if nodePool.Spec.Platform.AWS.Subnet != nil { subnet.ID = nodePool.Spec.Platform.AWS.Subnet.ID @@ -302,20 +285,21 @@ func generateScalableResources(client ctrlclient.Client, ctx context.Context, } subnet.Filters = append(subnet.Filters, filter) } - } else { - // TODO (alberto): remove hardcoded "a" zone and come up with a solution - // for automation across az - // e.g have a "locations" field in the nodeGroup or expose the subnet in the nodeGroup - subnet = &capiaws.AWSResourceReference{ - Filters: []capiaws.Filter{ - { - Name: "tag:Name", - Values: []string{ - fmt.Sprintf("%s-private-%sa", infraName, region), - }, - }, - }, + } + securityGroups := []capiaws.AWSResourceReference{} + for _, sg := range nodePool.Spec.Platform.AWS.SecurityGroups { + filters := []capiaws.Filter{} + for _, f := range sg.Filters { + filters = append(filters, capiaws.Filter{ + Name: f.Name, + Values: f.Values, + }) } + securityGroups = append(securityGroups, capiaws.AWSResourceReference{ + ARN: sg.ARN, + ID: sg.ID, + Filters: filters, + }) } instanceProfile := fmt.Sprintf("%s-worker-profile", infraName) @@ -344,9 +328,10 @@ func generateScalableResources(client ctrlclient.Client, ctx context.Context, IAMInstanceProfile: instanceProfile, InstanceType: instanceType, AMI: capiaws.AWSResourceReference{ - ID: k8sutilspointer.StringPtr(AMI), + ID: k8sutilspointer.StringPtr(ami), }, - Subnet: subnet, + AdditionalSecurityGroups: securityGroups, + Subnet: subnet, }, }, }, diff --git a/hypershift-operator/main.go b/hypershift-operator/main.go index 1dc6cc2b8111..fef337259415 100644 --- a/hypershift-operator/main.go +++ b/hypershift-operator/main.go @@ -32,6 +32,7 @@ import ( hyperapi "github.com/openshift/hypershift/api" "github.com/openshift/hypershift/hypershift-operator/controllers/externalinfracluster" "github.com/openshift/hypershift/hypershift-operator/controllers/hostedcluster" + "github.com/openshift/hypershift/hypershift-operator/controllers/machineimage/static" "github.com/openshift/hypershift/hypershift-operator/controllers/nodepool" ctrl "sigs.k8s.io/controller-runtime" @@ -147,7 +148,8 @@ func NewStartCommand() *cobra.Command { } if err := (&nodepool.NodePoolReconciler{ - Client: mgr.GetClient(), + Client: mgr.GetClient(), + ImageProvider: &static.StaticImageProvider{}, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "nodePool") os.Exit(1) diff --git a/thirdparty/clusterapi/api/v1alpha3/zz_generated.deepcopy.go b/thirdparty/clusterapi/api/v1alpha3/zz_generated.deepcopy.go index 9ecb3aeb3c5f..e2f246c63219 100644 --- a/thirdparty/clusterapi/api/v1alpha3/zz_generated.deepcopy.go +++ b/thirdparty/clusterapi/api/v1alpha3/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ // +build !ignore_autogenerated /* -Copyright The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ package v1alpha3 import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "github.com/openshift/hypershift/thirdparty/clusterapi/errors" diff --git a/thirdparty/clusterapi/api/v1alpha4/zz_generated.deepcopy.go b/thirdparty/clusterapi/api/v1alpha4/zz_generated.deepcopy.go index d33207f9c0e4..0b4f84bdb485 100644 --- a/thirdparty/clusterapi/api/v1alpha4/zz_generated.deepcopy.go +++ b/thirdparty/clusterapi/api/v1alpha4/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ // +build !ignore_autogenerated /* -Copyright The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ package v1alpha4 import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "github.com/openshift/hypershift/thirdparty/clusterapi/errors" diff --git a/thirdparty/clusterapiprovideraws/v1alpha3/zz_generated.deepcopy.go b/thirdparty/clusterapiprovideraws/v1alpha3/zz_generated.deepcopy.go index c3948ab80683..dbbfd58bf759 100644 --- a/thirdparty/clusterapiprovideraws/v1alpha3/zz_generated.deepcopy.go +++ b/thirdparty/clusterapiprovideraws/v1alpha3/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ // +build !ignore_autogenerated /* -Copyright The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vendor/gopkg.in/ini.v1/.gitignore b/vendor/gopkg.in/ini.v1/.gitignore new file mode 100644 index 000000000000..12411127b39e --- /dev/null +++ b/vendor/gopkg.in/ini.v1/.gitignore @@ -0,0 +1,6 @@ +testdata/conf_out.ini +ini.sublime-project +ini.sublime-workspace +testdata/conf_reflect.ini +.idea +/.vscode diff --git a/vendor/gopkg.in/ini.v1/.travis.yml b/vendor/gopkg.in/ini.v1/.travis.yml new file mode 100644 index 000000000000..149b7249f6a6 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/.travis.yml @@ -0,0 +1,20 @@ +sudo: false +language: go +go: + - 1.6.x + - 1.7.x + - 1.8.x + - 1.9.x + - 1.10.x + - 1.11.x + - 1.12.x + - 1.13.x + +install: skip +script: + - go get golang.org/x/tools/cmd/cover + - go get github.com/smartystreets/goconvey + - mkdir -p $HOME/gopath/src/gopkg.in + - ln -s $HOME/gopath/src/github.com/go-ini/ini $HOME/gopath/src/gopkg.in/ini.v1 + - cd $HOME/gopath/src/gopkg.in/ini.v1 + - go test -v -cover -race diff --git a/vendor/gopkg.in/ini.v1/LICENSE b/vendor/gopkg.in/ini.v1/LICENSE new file mode 100644 index 000000000000..d361bbcdf5c9 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright 2014 Unknwon + + 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. diff --git a/vendor/gopkg.in/ini.v1/Makefile b/vendor/gopkg.in/ini.v1/Makefile new file mode 100644 index 000000000000..af27ff0768fa --- /dev/null +++ b/vendor/gopkg.in/ini.v1/Makefile @@ -0,0 +1,15 @@ +.PHONY: build test bench vet coverage + +build: vet bench + +test: + go test -v -cover -race + +bench: + go test -v -cover -race -test.bench=. -test.benchmem + +vet: + go vet + +coverage: + go test -coverprofile=c.out && go tool cover -html=c.out && rm c.out diff --git a/vendor/gopkg.in/ini.v1/README.md b/vendor/gopkg.in/ini.v1/README.md new file mode 100644 index 000000000000..3d6d3cfc07b1 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/README.md @@ -0,0 +1,39 @@ +# INI + +[![Build Status](https://img.shields.io/travis/go-ini/ini/master.svg?style=for-the-badge&logo=travis)](https://travis-ci.org/go-ini/ini) [![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?style=for-the-badge&logo=sourcegraph)](https://sourcegraph.com/github.com/go-ini/ini) + +![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200) + +Package ini provides INI file read and write functionality in Go. + +## Features + +- Load from multiple data sources(`[]byte`, file and `io.ReadCloser`) with overwrites. +- Read with recursion values. +- Read with parent-child sections. +- Read with auto-increment key names. +- Read with multiple-line values. +- Read with tons of helper methods. +- Read and convert values to Go types. +- Read and **WRITE** comments of sections and keys. +- Manipulate sections, keys and comments with ease. +- Keep sections and keys in order as you parse and save. + +## Installation + +The minimum requirement of Go is **1.6**. + +```sh +$ go get gopkg.in/ini.v1 +``` + +Please add `-u` flag to update in the future. + +## Getting Help + +- [Getting Started](https://ini.unknwon.io/docs/intro/getting_started) +- [API Documentation](https://gowalker.org/gopkg.in/ini.v1) + +## License + +This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text. diff --git a/vendor/gopkg.in/ini.v1/data_source.go b/vendor/gopkg.in/ini.v1/data_source.go new file mode 100644 index 000000000000..dc0277ec6463 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/data_source.go @@ -0,0 +1,74 @@ +// Copyright 2019 Unknwon +// +// 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 ini + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" +) + +var ( + _ dataSource = (*sourceFile)(nil) + _ dataSource = (*sourceData)(nil) + _ dataSource = (*sourceReadCloser)(nil) +) + +// dataSource is an interface that returns object which can be read and closed. +type dataSource interface { + ReadCloser() (io.ReadCloser, error) +} + +// sourceFile represents an object that contains content on the local file system. +type sourceFile struct { + name string +} + +func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { + return os.Open(s.name) +} + +// sourceData represents an object that contains content in memory. +type sourceData struct { + data []byte +} + +func (s *sourceData) ReadCloser() (io.ReadCloser, error) { + return ioutil.NopCloser(bytes.NewReader(s.data)), nil +} + +// sourceReadCloser represents an input stream with Close method. +type sourceReadCloser struct { + reader io.ReadCloser +} + +func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) { + return s.reader, nil +} + +func parseDataSource(source interface{}) (dataSource, error) { + switch s := source.(type) { + case string: + return sourceFile{s}, nil + case []byte: + return &sourceData{s}, nil + case io.ReadCloser: + return &sourceReadCloser{s}, nil + default: + return nil, fmt.Errorf("error parsing data source: unknown type %q", s) + } +} diff --git a/vendor/gopkg.in/ini.v1/deprecated.go b/vendor/gopkg.in/ini.v1/deprecated.go new file mode 100644 index 000000000000..e8bda06e6ffe --- /dev/null +++ b/vendor/gopkg.in/ini.v1/deprecated.go @@ -0,0 +1,25 @@ +// Copyright 2019 Unknwon +// +// 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 ini + +const ( + // Deprecated: Use "DefaultSection" instead. + DEFAULT_SECTION = DefaultSection +) + +var ( + // Deprecated: AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE. + AllCapsUnderscore = SnackCase +) diff --git a/vendor/gopkg.in/ini.v1/error.go b/vendor/gopkg.in/ini.v1/error.go new file mode 100644 index 000000000000..d88347c54bf6 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/error.go @@ -0,0 +1,34 @@ +// Copyright 2016 Unknwon +// +// 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 ini + +import ( + "fmt" +) + +// ErrDelimiterNotFound indicates the error type of no delimiter is found which there should be one. +type ErrDelimiterNotFound struct { + Line string +} + +// IsErrDelimiterNotFound returns true if the given error is an instance of ErrDelimiterNotFound. +func IsErrDelimiterNotFound(err error) bool { + _, ok := err.(ErrDelimiterNotFound) + return ok +} + +func (err ErrDelimiterNotFound) Error() string { + return fmt.Sprintf("key-value delimiter not found: %s", err.Line) +} diff --git a/vendor/gopkg.in/ini.v1/file.go b/vendor/gopkg.in/ini.v1/file.go new file mode 100644 index 000000000000..017b77c8be0d --- /dev/null +++ b/vendor/gopkg.in/ini.v1/file.go @@ -0,0 +1,418 @@ +// Copyright 2017 Unknwon +// +// 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 ini + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "sync" +) + +// File represents a combination of a or more INI file(s) in memory. +type File struct { + options LoadOptions + dataSources []dataSource + + // Should make things safe, but sometimes doesn't matter. + BlockMode bool + lock sync.RWMutex + + // To keep data in order. + sectionList []string + // Actual data is stored here. + sections map[string]*Section + + NameMapper + ValueMapper +} + +// newFile initializes File object with given data sources. +func newFile(dataSources []dataSource, opts LoadOptions) *File { + if len(opts.KeyValueDelimiters) == 0 { + opts.KeyValueDelimiters = "=:" + } + return &File{ + BlockMode: true, + dataSources: dataSources, + sections: make(map[string]*Section), + sectionList: make([]string, 0, 10), + options: opts, + } +} + +// Empty returns an empty file object. +func Empty() *File { + // Ignore error here, we sure our data is good. + f, _ := Load([]byte("")) + return f +} + +// NewSection creates a new section. +func (f *File) NewSection(name string) (*Section, error) { + if len(name) == 0 { + return nil, errors.New("error creating new section: empty section name") + } else if f.options.Insensitive && name != DefaultSection { + name = strings.ToLower(name) + } + + if f.BlockMode { + f.lock.Lock() + defer f.lock.Unlock() + } + + if inSlice(name, f.sectionList) { + return f.sections[name], nil + } + + f.sectionList = append(f.sectionList, name) + f.sections[name] = newSection(f, name) + return f.sections[name], nil +} + +// NewRawSection creates a new section with an unparseable body. +func (f *File) NewRawSection(name, body string) (*Section, error) { + section, err := f.NewSection(name) + if err != nil { + return nil, err + } + + section.isRawSection = true + section.rawBody = body + return section, nil +} + +// NewSections creates a list of sections. +func (f *File) NewSections(names ...string) (err error) { + for _, name := range names { + if _, err = f.NewSection(name); err != nil { + return err + } + } + return nil +} + +// GetSection returns section by given name. +func (f *File) GetSection(name string) (*Section, error) { + if len(name) == 0 { + name = DefaultSection + } + if f.options.Insensitive { + name = strings.ToLower(name) + } + + if f.BlockMode { + f.lock.RLock() + defer f.lock.RUnlock() + } + + sec := f.sections[name] + if sec == nil { + return nil, fmt.Errorf("section '%s' does not exist", name) + } + return sec, nil +} + +// Section assumes named section exists and returns a zero-value when not. +func (f *File) Section(name string) *Section { + sec, err := f.GetSection(name) + if err != nil { + // Note: It's OK here because the only possible error is empty section name, + // but if it's empty, this piece of code won't be executed. + sec, _ = f.NewSection(name) + return sec + } + return sec +} + +// Sections returns a list of Section stored in the current instance. +func (f *File) Sections() []*Section { + if f.BlockMode { + f.lock.RLock() + defer f.lock.RUnlock() + } + + sections := make([]*Section, len(f.sectionList)) + for i, name := range f.sectionList { + sections[i] = f.sections[name] + } + return sections +} + +// ChildSections returns a list of child sections of given section name. +func (f *File) ChildSections(name string) []*Section { + return f.Section(name).ChildSections() +} + +// SectionStrings returns list of section names. +func (f *File) SectionStrings() []string { + list := make([]string, len(f.sectionList)) + copy(list, f.sectionList) + return list +} + +// DeleteSection deletes a section. +func (f *File) DeleteSection(name string) { + if f.BlockMode { + f.lock.Lock() + defer f.lock.Unlock() + } + + if len(name) == 0 { + name = DefaultSection + } + + for i, s := range f.sectionList { + if s == name { + f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...) + delete(f.sections, name) + return + } + } +} + +func (f *File) reload(s dataSource) error { + r, err := s.ReadCloser() + if err != nil { + return err + } + defer r.Close() + + return f.parse(r) +} + +// Reload reloads and parses all data sources. +func (f *File) Reload() (err error) { + for _, s := range f.dataSources { + if err = f.reload(s); err != nil { + // In loose mode, we create an empty default section for nonexistent files. + if os.IsNotExist(err) && f.options.Loose { + f.parse(bytes.NewBuffer(nil)) + continue + } + return err + } + } + return nil +} + +// Append appends one or more data sources and reloads automatically. +func (f *File) Append(source interface{}, others ...interface{}) error { + ds, err := parseDataSource(source) + if err != nil { + return err + } + f.dataSources = append(f.dataSources, ds) + for _, s := range others { + ds, err = parseDataSource(s) + if err != nil { + return err + } + f.dataSources = append(f.dataSources, ds) + } + return f.Reload() +} + +func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) { + equalSign := DefaultFormatLeft + "=" + DefaultFormatRight + + if PrettyFormat || PrettyEqual { + equalSign = " = " + } + + // Use buffer to make sure target is safe until finish encoding. + buf := bytes.NewBuffer(nil) + for i, sname := range f.sectionList { + sec := f.Section(sname) + if len(sec.Comment) > 0 { + // Support multiline comments + lines := strings.Split(sec.Comment, LineBreak) + for i := range lines { + if lines[i][0] != '#' && lines[i][0] != ';' { + lines[i] = "; " + lines[i] + } else { + lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:]) + } + + if _, err := buf.WriteString(lines[i] + LineBreak); err != nil { + return nil, err + } + } + } + + if i > 0 || DefaultHeader { + if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil { + return nil, err + } + } else { + // Write nothing if default section is empty + if len(sec.keyList) == 0 { + continue + } + } + + if sec.isRawSection { + if _, err := buf.WriteString(sec.rawBody); err != nil { + return nil, err + } + + if PrettySection { + // Put a line between sections + if _, err := buf.WriteString(LineBreak); err != nil { + return nil, err + } + } + continue + } + + // Count and generate alignment length and buffer spaces using the + // longest key. Keys may be modifed if they contain certain characters so + // we need to take that into account in our calculation. + alignLength := 0 + if PrettyFormat { + for _, kname := range sec.keyList { + keyLength := len(kname) + // First case will surround key by ` and second by """ + if strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters) { + keyLength += 2 + } else if strings.Contains(kname, "`") { + keyLength += 6 + } + + if keyLength > alignLength { + alignLength = keyLength + } + } + } + alignSpaces := bytes.Repeat([]byte(" "), alignLength) + + KeyList: + for _, kname := range sec.keyList { + key := sec.Key(kname) + if len(key.Comment) > 0 { + if len(indent) > 0 && sname != DefaultSection { + buf.WriteString(indent) + } + + // Support multiline comments + lines := strings.Split(key.Comment, LineBreak) + for i := range lines { + if lines[i][0] != '#' && lines[i][0] != ';' { + lines[i] = "; " + strings.TrimSpace(lines[i]) + } else { + lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:]) + } + + if _, err := buf.WriteString(lines[i] + LineBreak); err != nil { + return nil, err + } + } + } + + if len(indent) > 0 && sname != DefaultSection { + buf.WriteString(indent) + } + + switch { + case key.isAutoIncrement: + kname = "-" + case strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters): + kname = "`" + kname + "`" + case strings.Contains(kname, "`"): + kname = `"""` + kname + `"""` + } + + for _, val := range key.ValueWithShadows() { + if _, err := buf.WriteString(kname); err != nil { + return nil, err + } + + if key.isBooleanType { + if kname != sec.keyList[len(sec.keyList)-1] { + buf.WriteString(LineBreak) + } + continue KeyList + } + + // Write out alignment spaces before "=" sign + if PrettyFormat { + buf.Write(alignSpaces[:alignLength-len(kname)]) + } + + // In case key value contains "\n", "`", "\"", "#" or ";" + if strings.ContainsAny(val, "\n`") { + val = `"""` + val + `"""` + } else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") { + val = "`" + val + "`" + } + if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil { + return nil, err + } + } + + for _, val := range key.nestedValues { + if _, err := buf.WriteString(indent + " " + val + LineBreak); err != nil { + return nil, err + } + } + } + + if PrettySection { + // Put a line between sections + if _, err := buf.WriteString(LineBreak); err != nil { + return nil, err + } + } + } + + return buf, nil +} + +// WriteToIndent writes content into io.Writer with given indention. +// If PrettyFormat has been set to be true, +// it will align "=" sign with spaces under each section. +func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) { + buf, err := f.writeToBuffer(indent) + if err != nil { + return 0, err + } + return buf.WriteTo(w) +} + +// WriteTo writes file content into io.Writer. +func (f *File) WriteTo(w io.Writer) (int64, error) { + return f.WriteToIndent(w, "") +} + +// SaveToIndent writes content to file system with given value indention. +func (f *File) SaveToIndent(filename, indent string) error { + // Note: Because we are truncating with os.Create, + // so it's safer to save to a temporary file location and rename afte done. + buf, err := f.writeToBuffer(indent) + if err != nil { + return err + } + + return ioutil.WriteFile(filename, buf.Bytes(), 0666) +} + +// SaveTo writes content to file system. +func (f *File) SaveTo(filename string) error { + return f.SaveToIndent(filename, "") +} diff --git a/vendor/gopkg.in/ini.v1/helper.go b/vendor/gopkg.in/ini.v1/helper.go new file mode 100644 index 000000000000..f9d80a682a55 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/helper.go @@ -0,0 +1,24 @@ +// Copyright 2019 Unknwon +// +// 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 ini + +func inSlice(str string, s []string) bool { + for _, v := range s { + if str == v { + return true + } + } + return false +} diff --git a/vendor/gopkg.in/ini.v1/ini.go b/vendor/gopkg.in/ini.v1/ini.go new file mode 100644 index 000000000000..945fc00c0fdf --- /dev/null +++ b/vendor/gopkg.in/ini.v1/ini.go @@ -0,0 +1,166 @@ +// +build go1.6 + +// Copyright 2014 Unknwon +// +// 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 ini provides INI file read and write functionality in Go. +package ini + +import ( + "regexp" + "runtime" +) + +const ( + // DefaultSection is the name of default section. You can use this constant or the string literal. + // In most of cases, an empty string is all you need to access the section. + DefaultSection = "DEFAULT" + + // Maximum allowed depth when recursively substituing variable names. + depthValues = 99 + version = "1.51.0" +) + +// Version returns current package version literal. +func Version() string { + return version +} + +var ( + // LineBreak is the delimiter to determine or compose a new line. + // This variable will be changed to "\r\n" automatically on Windows at package init time. + LineBreak = "\n" + + // Variable regexp pattern: %(variable)s + varPattern = regexp.MustCompile(`%\(([^)]+)\)s`) + + // DefaultHeader explicitly writes default section header. + DefaultHeader = false + + // PrettySection indicates whether to put a line between sections. + PrettySection = true + // PrettyFormat indicates whether to align "=" sign with spaces to produce pretty output + // or reduce all possible spaces for compact format. + PrettyFormat = true + // PrettyEqual places spaces around "=" sign even when PrettyFormat is false. + PrettyEqual = false + // DefaultFormatLeft places custom spaces on the left when PrettyFormat and PrettyEqual are both disabled. + DefaultFormatLeft = "" + // DefaultFormatRight places custom spaces on the right when PrettyFormat and PrettyEqual are both disabled. + DefaultFormatRight = "" +) + +func init() { + if runtime.GOOS == "windows" { + LineBreak = "\r\n" + } +} + +// LoadOptions contains all customized options used for load data source(s). +type LoadOptions struct { + // Loose indicates whether the parser should ignore nonexistent files or return error. + Loose bool + // Insensitive indicates whether the parser forces all section and key names to lowercase. + Insensitive bool + // IgnoreContinuation indicates whether to ignore continuation lines while parsing. + IgnoreContinuation bool + // IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value. + IgnoreInlineComment bool + // SkipUnrecognizableLines indicates whether to skip unrecognizable lines that do not conform to key/value pairs. + SkipUnrecognizableLines bool + // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing. + // This type of keys are mostly used in my.cnf. + AllowBooleanKeys bool + // AllowShadows indicates whether to keep track of keys with same name under same section. + AllowShadows bool + // AllowNestedValues indicates whether to allow AWS-like nested values. + // Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values + AllowNestedValues bool + // AllowPythonMultilineValues indicates whether to allow Python-like multi-line values. + // Docs: https://docs.python.org/3/library/configparser.html#supported-ini-file-structure + // Relevant quote: Values can also span multiple lines, as long as they are indented deeper + // than the first line of the value. + AllowPythonMultilineValues bool + // SpaceBeforeInlineComment indicates whether to allow comment symbols (\# and \;) inside value. + // Docs: https://docs.python.org/2/library/configparser.html + // Quote: Comments may appear on their own in an otherwise empty line, or may be entered in lines holding values or section names. + // In the latter case, they need to be preceded by a whitespace character to be recognized as a comment. + SpaceBeforeInlineComment bool + // UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format + // when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value" + UnescapeValueDoubleQuotes bool + // UnescapeValueCommentSymbols indicates to unescape comment symbols (\# and \;) inside value to regular format + // when value is NOT surrounded by any quotes. + // Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all. + UnescapeValueCommentSymbols bool + // UnparseableSections stores a list of blocks that are allowed with raw content which do not otherwise + // conform to key/value pairs. Specify the names of those blocks here. + UnparseableSections []string + // KeyValueDelimiters is the sequence of delimiters that are used to separate key and value. By default, it is "=:". + KeyValueDelimiters string + // PreserveSurroundedQuote indicates whether to preserve surrounded quote (single and double quotes). + PreserveSurroundedQuote bool + // DebugFunc is called to collect debug information (currently only useful to debug parsing Python-style multiline values). + DebugFunc DebugFunc + // ReaderBufferSize is the buffer size of the reader in bytes. + ReaderBufferSize int +} + +// DebugFunc is the type of function called to log parse events. +type DebugFunc func(message string) + +// LoadSources allows caller to apply customized options for loading from data source(s). +func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) { + sources := make([]dataSource, len(others)+1) + sources[0], err = parseDataSource(source) + if err != nil { + return nil, err + } + for i := range others { + sources[i+1], err = parseDataSource(others[i]) + if err != nil { + return nil, err + } + } + f := newFile(sources, opts) + if err = f.Reload(); err != nil { + return nil, err + } + return f, nil +} + +// Load loads and parses from INI data sources. +// Arguments can be mixed of file name with string type, or raw data in []byte. +// It will return error if list contains nonexistent files. +func Load(source interface{}, others ...interface{}) (*File, error) { + return LoadSources(LoadOptions{}, source, others...) +} + +// LooseLoad has exactly same functionality as Load function +// except it ignores nonexistent files instead of returning error. +func LooseLoad(source interface{}, others ...interface{}) (*File, error) { + return LoadSources(LoadOptions{Loose: true}, source, others...) +} + +// InsensitiveLoad has exactly same functionality as Load function +// except it forces all section and key names to be lowercased. +func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) { + return LoadSources(LoadOptions{Insensitive: true}, source, others...) +} + +// ShadowLoad has exactly same functionality as Load function +// except it allows have shadow keys. +func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { + return LoadSources(LoadOptions{AllowShadows: true}, source, others...) +} diff --git a/vendor/gopkg.in/ini.v1/key.go b/vendor/gopkg.in/ini.v1/key.go new file mode 100644 index 000000000000..3c197410fa56 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/key.go @@ -0,0 +1,801 @@ +// Copyright 2014 Unknwon +// +// 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 ini + +import ( + "bytes" + "errors" + "fmt" + "strconv" + "strings" + "time" +) + +// Key represents a key under a section. +type Key struct { + s *Section + Comment string + name string + value string + isAutoIncrement bool + isBooleanType bool + + isShadow bool + shadows []*Key + + nestedValues []string +} + +// newKey simply return a key object with given values. +func newKey(s *Section, name, val string) *Key { + return &Key{ + s: s, + name: name, + value: val, + } +} + +func (k *Key) addShadow(val string) error { + if k.isShadow { + return errors.New("cannot add shadow to another shadow key") + } else if k.isAutoIncrement || k.isBooleanType { + return errors.New("cannot add shadow to auto-increment or boolean key") + } + + // Deduplicate shadows based on their values. + if k.value == val { + return nil + } + for i := range k.shadows { + if k.shadows[i].value == val { + return nil + } + } + + shadow := newKey(k.s, k.name, val) + shadow.isShadow = true + k.shadows = append(k.shadows, shadow) + return nil +} + +// AddShadow adds a new shadow key to itself. +func (k *Key) AddShadow(val string) error { + if !k.s.f.options.AllowShadows { + return errors.New("shadow key is not allowed") + } + return k.addShadow(val) +} + +func (k *Key) addNestedValue(val string) error { + if k.isAutoIncrement || k.isBooleanType { + return errors.New("cannot add nested value to auto-increment or boolean key") + } + + k.nestedValues = append(k.nestedValues, val) + return nil +} + +// AddNestedValue adds a nested value to the key. +func (k *Key) AddNestedValue(val string) error { + if !k.s.f.options.AllowNestedValues { + return errors.New("nested value is not allowed") + } + return k.addNestedValue(val) +} + +// ValueMapper represents a mapping function for values, e.g. os.ExpandEnv +type ValueMapper func(string) string + +// Name returns name of key. +func (k *Key) Name() string { + return k.name +} + +// Value returns raw value of key for performance purpose. +func (k *Key) Value() string { + return k.value +} + +// ValueWithShadows returns raw values of key and its shadows if any. +func (k *Key) ValueWithShadows() []string { + if len(k.shadows) == 0 { + return []string{k.value} + } + vals := make([]string, len(k.shadows)+1) + vals[0] = k.value + for i := range k.shadows { + vals[i+1] = k.shadows[i].value + } + return vals +} + +// NestedValues returns nested values stored in the key. +// It is possible returned value is nil if no nested values stored in the key. +func (k *Key) NestedValues() []string { + return k.nestedValues +} + +// transformValue takes a raw value and transforms to its final string. +func (k *Key) transformValue(val string) string { + if k.s.f.ValueMapper != nil { + val = k.s.f.ValueMapper(val) + } + + // Fail-fast if no indicate char found for recursive value + if !strings.Contains(val, "%") { + return val + } + for i := 0; i < depthValues; i++ { + vr := varPattern.FindString(val) + if len(vr) == 0 { + break + } + + // Take off leading '%(' and trailing ')s'. + noption := vr[2 : len(vr)-2] + + // Search in the same section. + // If not found or found the key itself, then search again in default section. + nk, err := k.s.GetKey(noption) + if err != nil || k == nk { + nk, _ = k.s.f.Section("").GetKey(noption) + if nk == nil { + // Stop when no results found in the default section, + // and returns the value as-is. + break + } + } + + // Substitute by new value and take off leading '%(' and trailing ')s'. + val = strings.Replace(val, vr, nk.value, -1) + } + return val +} + +// String returns string representation of value. +func (k *Key) String() string { + return k.transformValue(k.value) +} + +// Validate accepts a validate function which can +// return modifed result as key value. +func (k *Key) Validate(fn func(string) string) string { + return fn(k.String()) +} + +// parseBool returns the boolean value represented by the string. +// +// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On, +// 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off. +// Any other value returns an error. +func parseBool(str string) (value bool, err error) { + switch str { + case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On": + return true, nil + case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off": + return false, nil + } + return false, fmt.Errorf("parsing \"%s\": invalid syntax", str) +} + +// Bool returns bool type value. +func (k *Key) Bool() (bool, error) { + return parseBool(k.String()) +} + +// Float64 returns float64 type value. +func (k *Key) Float64() (float64, error) { + return strconv.ParseFloat(k.String(), 64) +} + +// Int returns int type value. +func (k *Key) Int() (int, error) { + v, err := strconv.ParseInt(k.String(), 0, 64) + return int(v), err +} + +// Int64 returns int64 type value. +func (k *Key) Int64() (int64, error) { + return strconv.ParseInt(k.String(), 0, 64) +} + +// Uint returns uint type valued. +func (k *Key) Uint() (uint, error) { + u, e := strconv.ParseUint(k.String(), 0, 64) + return uint(u), e +} + +// Uint64 returns uint64 type value. +func (k *Key) Uint64() (uint64, error) { + return strconv.ParseUint(k.String(), 0, 64) +} + +// Duration returns time.Duration type value. +func (k *Key) Duration() (time.Duration, error) { + return time.ParseDuration(k.String()) +} + +// TimeFormat parses with given format and returns time.Time type value. +func (k *Key) TimeFormat(format string) (time.Time, error) { + return time.Parse(format, k.String()) +} + +// Time parses with RFC3339 format and returns time.Time type value. +func (k *Key) Time() (time.Time, error) { + return k.TimeFormat(time.RFC3339) +} + +// MustString returns default value if key value is empty. +func (k *Key) MustString(defaultVal string) string { + val := k.String() + if len(val) == 0 { + k.value = defaultVal + return defaultVal + } + return val +} + +// MustBool always returns value without error, +// it returns false if error occurs. +func (k *Key) MustBool(defaultVal ...bool) bool { + val, err := k.Bool() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatBool(defaultVal[0]) + return defaultVal[0] + } + return val +} + +// MustFloat64 always returns value without error, +// it returns 0.0 if error occurs. +func (k *Key) MustFloat64(defaultVal ...float64) float64 { + val, err := k.Float64() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64) + return defaultVal[0] + } + return val +} + +// MustInt always returns value without error, +// it returns 0 if error occurs. +func (k *Key) MustInt(defaultVal ...int) int { + val, err := k.Int() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatInt(int64(defaultVal[0]), 10) + return defaultVal[0] + } + return val +} + +// MustInt64 always returns value without error, +// it returns 0 if error occurs. +func (k *Key) MustInt64(defaultVal ...int64) int64 { + val, err := k.Int64() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatInt(defaultVal[0], 10) + return defaultVal[0] + } + return val +} + +// MustUint always returns value without error, +// it returns 0 if error occurs. +func (k *Key) MustUint(defaultVal ...uint) uint { + val, err := k.Uint() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatUint(uint64(defaultVal[0]), 10) + return defaultVal[0] + } + return val +} + +// MustUint64 always returns value without error, +// it returns 0 if error occurs. +func (k *Key) MustUint64(defaultVal ...uint64) uint64 { + val, err := k.Uint64() + if len(defaultVal) > 0 && err != nil { + k.value = strconv.FormatUint(defaultVal[0], 10) + return defaultVal[0] + } + return val +} + +// MustDuration always returns value without error, +// it returns zero value if error occurs. +func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration { + val, err := k.Duration() + if len(defaultVal) > 0 && err != nil { + k.value = defaultVal[0].String() + return defaultVal[0] + } + return val +} + +// MustTimeFormat always parses with given format and returns value without error, +// it returns zero value if error occurs. +func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time { + val, err := k.TimeFormat(format) + if len(defaultVal) > 0 && err != nil { + k.value = defaultVal[0].Format(format) + return defaultVal[0] + } + return val +} + +// MustTime always parses with RFC3339 format and returns value without error, +// it returns zero value if error occurs. +func (k *Key) MustTime(defaultVal ...time.Time) time.Time { + return k.MustTimeFormat(time.RFC3339, defaultVal...) +} + +// In always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) In(defaultVal string, candidates []string) string { + val := k.String() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InFloat64 always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 { + val := k.MustFloat64() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InInt always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InInt(defaultVal int, candidates []int) int { + val := k.MustInt() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InInt64 always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 { + val := k.MustInt64() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InUint always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InUint(defaultVal uint, candidates []uint) uint { + val := k.MustUint() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InUint64 always returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 { + val := k.MustUint64() + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InTimeFormat always parses with given format and returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time { + val := k.MustTimeFormat(format) + for _, cand := range candidates { + if val == cand { + return val + } + } + return defaultVal +} + +// InTime always parses with RFC3339 format and returns value without error, +// it returns default value if error occurs or doesn't fit into candidates. +func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time { + return k.InTimeFormat(time.RFC3339, defaultVal, candidates) +} + +// RangeFloat64 checks if value is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 { + val := k.MustFloat64() + if val < min || val > max { + return defaultVal + } + return val +} + +// RangeInt checks if value is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeInt(defaultVal, min, max int) int { + val := k.MustInt() + if val < min || val > max { + return defaultVal + } + return val +} + +// RangeInt64 checks if value is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeInt64(defaultVal, min, max int64) int64 { + val := k.MustInt64() + if val < min || val > max { + return defaultVal + } + return val +} + +// RangeTimeFormat checks if value with given format is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time { + val := k.MustTimeFormat(format) + if val.Unix() < min.Unix() || val.Unix() > max.Unix() { + return defaultVal + } + return val +} + +// RangeTime checks if value with RFC3339 format is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time { + return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max) +} + +// Strings returns list of string divided by given delimiter. +func (k *Key) Strings(delim string) []string { + str := k.String() + if len(str) == 0 { + return []string{} + } + + runes := []rune(str) + vals := make([]string, 0, 2) + var buf bytes.Buffer + escape := false + idx := 0 + for { + if escape { + escape = false + if runes[idx] != '\\' && !strings.HasPrefix(string(runes[idx:]), delim) { + buf.WriteRune('\\') + } + buf.WriteRune(runes[idx]) + } else { + if runes[idx] == '\\' { + escape = true + } else if strings.HasPrefix(string(runes[idx:]), delim) { + idx += len(delim) - 1 + vals = append(vals, strings.TrimSpace(buf.String())) + buf.Reset() + } else { + buf.WriteRune(runes[idx]) + } + } + idx++ + if idx == len(runes) { + break + } + } + + if buf.Len() > 0 { + vals = append(vals, strings.TrimSpace(buf.String())) + } + + return vals +} + +// StringsWithShadows returns list of string divided by given delimiter. +// Shadows will also be appended if any. +func (k *Key) StringsWithShadows(delim string) []string { + vals := k.ValueWithShadows() + results := make([]string, 0, len(vals)*2) + for i := range vals { + if len(vals) == 0 { + continue + } + + results = append(results, strings.Split(vals[i], delim)...) + } + + for i := range results { + results[i] = k.transformValue(strings.TrimSpace(results[i])) + } + return results +} + +// Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Float64s(delim string) []float64 { + vals, _ := k.parseFloat64s(k.Strings(delim), true, false) + return vals +} + +// Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Ints(delim string) []int { + vals, _ := k.parseInts(k.Strings(delim), true, false) + return vals +} + +// Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Int64s(delim string) []int64 { + vals, _ := k.parseInt64s(k.Strings(delim), true, false) + return vals +} + +// Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Uints(delim string) []uint { + vals, _ := k.parseUints(k.Strings(delim), true, false) + return vals +} + +// Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Uint64s(delim string) []uint64 { + vals, _ := k.parseUint64s(k.Strings(delim), true, false) + return vals +} + +// Bools returns list of bool divided by given delimiter. Any invalid input will be treated as zero value. +func (k *Key) Bools(delim string) []bool { + vals, _ := k.parseBools(k.Strings(delim), true, false) + return vals +} + +// TimesFormat parses with given format and returns list of time.Time divided by given delimiter. +// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). +func (k *Key) TimesFormat(format, delim string) []time.Time { + vals, _ := k.parseTimesFormat(format, k.Strings(delim), true, false) + return vals +} + +// Times parses with RFC3339 format and returns list of time.Time divided by given delimiter. +// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). +func (k *Key) Times(delim string) []time.Time { + return k.TimesFormat(time.RFC3339, delim) +} + +// ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then +// it will not be included to result list. +func (k *Key) ValidFloat64s(delim string) []float64 { + vals, _ := k.parseFloat64s(k.Strings(delim), false, false) + return vals +} + +// ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will +// not be included to result list. +func (k *Key) ValidInts(delim string) []int { + vals, _ := k.parseInts(k.Strings(delim), false, false) + return vals +} + +// ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer, +// then it will not be included to result list. +func (k *Key) ValidInt64s(delim string) []int64 { + vals, _ := k.parseInt64s(k.Strings(delim), false, false) + return vals +} + +// ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer, +// then it will not be included to result list. +func (k *Key) ValidUints(delim string) []uint { + vals, _ := k.parseUints(k.Strings(delim), false, false) + return vals +} + +// ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned +// integer, then it will not be included to result list. +func (k *Key) ValidUint64s(delim string) []uint64 { + vals, _ := k.parseUint64s(k.Strings(delim), false, false) + return vals +} + +// ValidBools returns list of bool divided by given delimiter. If some value is not 64-bit unsigned +// integer, then it will not be included to result list. +func (k *Key) ValidBools(delim string) []bool { + vals, _ := k.parseBools(k.Strings(delim), false, false) + return vals +} + +// ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter. +func (k *Key) ValidTimesFormat(format, delim string) []time.Time { + vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false) + return vals +} + +// ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter. +func (k *Key) ValidTimes(delim string) []time.Time { + return k.ValidTimesFormat(time.RFC3339, delim) +} + +// StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input. +func (k *Key) StrictFloat64s(delim string) ([]float64, error) { + return k.parseFloat64s(k.Strings(delim), false, true) +} + +// StrictInts returns list of int divided by given delimiter or error on first invalid input. +func (k *Key) StrictInts(delim string) ([]int, error) { + return k.parseInts(k.Strings(delim), false, true) +} + +// StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input. +func (k *Key) StrictInt64s(delim string) ([]int64, error) { + return k.parseInt64s(k.Strings(delim), false, true) +} + +// StrictUints returns list of uint divided by given delimiter or error on first invalid input. +func (k *Key) StrictUints(delim string) ([]uint, error) { + return k.parseUints(k.Strings(delim), false, true) +} + +// StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input. +func (k *Key) StrictUint64s(delim string) ([]uint64, error) { + return k.parseUint64s(k.Strings(delim), false, true) +} + +// StrictBools returns list of bool divided by given delimiter or error on first invalid input. +func (k *Key) StrictBools(delim string) ([]bool, error) { + return k.parseBools(k.Strings(delim), false, true) +} + +// StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter +// or error on first invalid input. +func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) { + return k.parseTimesFormat(format, k.Strings(delim), false, true) +} + +// StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter +// or error on first invalid input. +func (k *Key) StrictTimes(delim string) ([]time.Time, error) { + return k.StrictTimesFormat(time.RFC3339, delim) +} + +// parseBools transforms strings to bools. +func (k *Key) parseBools(strs []string, addInvalid, returnOnInvalid bool) ([]bool, error) { + vals := make([]bool, 0, len(strs)) + for _, str := range strs { + val, err := parseBool(str) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// parseFloat64s transforms strings to float64s. +func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) { + vals := make([]float64, 0, len(strs)) + for _, str := range strs { + val, err := strconv.ParseFloat(str, 64) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// parseInts transforms strings to ints. +func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) { + vals := make([]int, 0, len(strs)) + for _, str := range strs { + valInt64, err := strconv.ParseInt(str, 0, 64) + val := int(valInt64) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// parseInt64s transforms strings to int64s. +func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) { + vals := make([]int64, 0, len(strs)) + for _, str := range strs { + val, err := strconv.ParseInt(str, 0, 64) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// parseUints transforms strings to uints. +func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) { + vals := make([]uint, 0, len(strs)) + for _, str := range strs { + val, err := strconv.ParseUint(str, 0, 0) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, uint(val)) + } + } + return vals, nil +} + +// parseUint64s transforms strings to uint64s. +func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) { + vals := make([]uint64, 0, len(strs)) + for _, str := range strs { + val, err := strconv.ParseUint(str, 0, 64) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// parseTimesFormat transforms strings to times in given format. +func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) { + vals := make([]time.Time, 0, len(strs)) + for _, str := range strs { + val, err := time.Parse(format, str) + if err != nil && returnOnInvalid { + return nil, err + } + if err == nil || addInvalid { + vals = append(vals, val) + } + } + return vals, nil +} + +// SetValue changes key value. +func (k *Key) SetValue(v string) { + if k.s.f.BlockMode { + k.s.f.lock.Lock() + defer k.s.f.lock.Unlock() + } + + k.value = v + k.s.keysHash[k.name] = v +} diff --git a/vendor/gopkg.in/ini.v1/parser.go b/vendor/gopkg.in/ini.v1/parser.go new file mode 100644 index 000000000000..53ab45c46fcf --- /dev/null +++ b/vendor/gopkg.in/ini.v1/parser.go @@ -0,0 +1,526 @@ +// Copyright 2015 Unknwon +// +// 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 ini + +import ( + "bufio" + "bytes" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "unicode" +) + +const minReaderBufferSize = 4096 + +var pythonMultiline = regexp.MustCompile(`^([\t\f ]+)(.*)`) + +type parserOptions struct { + IgnoreContinuation bool + IgnoreInlineComment bool + AllowPythonMultilineValues bool + SpaceBeforeInlineComment bool + UnescapeValueDoubleQuotes bool + UnescapeValueCommentSymbols bool + PreserveSurroundedQuote bool + DebugFunc DebugFunc + ReaderBufferSize int +} + +type parser struct { + buf *bufio.Reader + options parserOptions + + isEOF bool + count int + comment *bytes.Buffer +} + +func (p *parser) debug(format string, args ...interface{}) { + if p.options.DebugFunc != nil { + p.options.DebugFunc(fmt.Sprintf(format, args...)) + } +} + +func newParser(r io.Reader, opts parserOptions) *parser { + size := opts.ReaderBufferSize + if size < minReaderBufferSize { + size = minReaderBufferSize + } + + return &parser{ + buf: bufio.NewReaderSize(r, size), + options: opts, + count: 1, + comment: &bytes.Buffer{}, + } +} + +// BOM handles header of UTF-8, UTF-16 LE and UTF-16 BE's BOM format. +// http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding +func (p *parser) BOM() error { + mask, err := p.buf.Peek(2) + if err != nil && err != io.EOF { + return err + } else if len(mask) < 2 { + return nil + } + + switch { + case mask[0] == 254 && mask[1] == 255: + fallthrough + case mask[0] == 255 && mask[1] == 254: + p.buf.Read(mask) + case mask[0] == 239 && mask[1] == 187: + mask, err := p.buf.Peek(3) + if err != nil && err != io.EOF { + return err + } else if len(mask) < 3 { + return nil + } + if mask[2] == 191 { + p.buf.Read(mask) + } + } + return nil +} + +func (p *parser) readUntil(delim byte) ([]byte, error) { + data, err := p.buf.ReadBytes(delim) + if err != nil { + if err == io.EOF { + p.isEOF = true + } else { + return nil, err + } + } + return data, nil +} + +func cleanComment(in []byte) ([]byte, bool) { + i := bytes.IndexAny(in, "#;") + if i == -1 { + return nil, false + } + return in[i:], true +} + +func readKeyName(delimiters string, in []byte) (string, int, error) { + line := string(in) + + // Check if key name surrounded by quotes. + var keyQuote string + if line[0] == '"' { + if len(line) > 6 && string(line[0:3]) == `"""` { + keyQuote = `"""` + } else { + keyQuote = `"` + } + } else if line[0] == '`' { + keyQuote = "`" + } + + // Get out key name + endIdx := -1 + if len(keyQuote) > 0 { + startIdx := len(keyQuote) + // FIXME: fail case -> """"""name"""=value + pos := strings.Index(line[startIdx:], keyQuote) + if pos == -1 { + return "", -1, fmt.Errorf("missing closing key quote: %s", line) + } + pos += startIdx + + // Find key-value delimiter + i := strings.IndexAny(line[pos+startIdx:], delimiters) + if i < 0 { + return "", -1, ErrDelimiterNotFound{line} + } + endIdx = pos + i + return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil + } + + endIdx = strings.IndexAny(line, delimiters) + if endIdx < 0 { + return "", -1, ErrDelimiterNotFound{line} + } + return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil +} + +func (p *parser) readMultilines(line, val, valQuote string) (string, error) { + for { + data, err := p.readUntil('\n') + if err != nil { + return "", err + } + next := string(data) + + pos := strings.LastIndex(next, valQuote) + if pos > -1 { + val += next[:pos] + + comment, has := cleanComment([]byte(next[pos:])) + if has { + p.comment.Write(bytes.TrimSpace(comment)) + } + break + } + val += next + if p.isEOF { + return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next) + } + } + return val, nil +} + +func (p *parser) readContinuationLines(val string) (string, error) { + for { + data, err := p.readUntil('\n') + if err != nil { + return "", err + } + next := strings.TrimSpace(string(data)) + + if len(next) == 0 { + break + } + val += next + if val[len(val)-1] != '\\' { + break + } + val = val[:len(val)-1] + } + return val, nil +} + +// hasSurroundedQuote check if and only if the first and last characters +// are quotes \" or \'. +// It returns false if any other parts also contain same kind of quotes. +func hasSurroundedQuote(in string, quote byte) bool { + return len(in) >= 2 && in[0] == quote && in[len(in)-1] == quote && + strings.IndexByte(in[1:], quote) == len(in)-2 +} + +func (p *parser) readValue(in []byte, bufferSize int) (string, error) { + + line := strings.TrimLeftFunc(string(in), unicode.IsSpace) + if len(line) == 0 { + if p.options.AllowPythonMultilineValues && len(in) > 0 && in[len(in)-1] == '\n' { + return p.readPythonMultilines(line, bufferSize) + } + return "", nil + } + + var valQuote string + if len(line) > 3 && string(line[0:3]) == `"""` { + valQuote = `"""` + } else if line[0] == '`' { + valQuote = "`" + } else if p.options.UnescapeValueDoubleQuotes && line[0] == '"' { + valQuote = `"` + } + + if len(valQuote) > 0 { + startIdx := len(valQuote) + pos := strings.LastIndex(line[startIdx:], valQuote) + // Check for multi-line value + if pos == -1 { + return p.readMultilines(line, line[startIdx:], valQuote) + } + + if p.options.UnescapeValueDoubleQuotes && valQuote == `"` { + return strings.Replace(line[startIdx:pos+startIdx], `\"`, `"`, -1), nil + } + return line[startIdx : pos+startIdx], nil + } + + lastChar := line[len(line)-1] + // Won't be able to reach here if value only contains whitespace + line = strings.TrimSpace(line) + trimmedLastChar := line[len(line)-1] + + // Check continuation lines when desired + if !p.options.IgnoreContinuation && trimmedLastChar == '\\' { + return p.readContinuationLines(line[:len(line)-1]) + } + + // Check if ignore inline comment + if !p.options.IgnoreInlineComment { + var i int + if p.options.SpaceBeforeInlineComment { + i = strings.Index(line, " #") + if i == -1 { + i = strings.Index(line, " ;") + } + + } else { + i = strings.IndexAny(line, "#;") + } + + if i > -1 { + p.comment.WriteString(line[i:]) + line = strings.TrimSpace(line[:i]) + } + + } + + // Trim single and double quotes + if (hasSurroundedQuote(line, '\'') || + hasSurroundedQuote(line, '"')) && !p.options.PreserveSurroundedQuote { + line = line[1 : len(line)-1] + } else if len(valQuote) == 0 && p.options.UnescapeValueCommentSymbols { + if strings.Contains(line, `\;`) { + line = strings.Replace(line, `\;`, ";", -1) + } + if strings.Contains(line, `\#`) { + line = strings.Replace(line, `\#`, "#", -1) + } + } else if p.options.AllowPythonMultilineValues && lastChar == '\n' { + return p.readPythonMultilines(line, bufferSize) + } + + return line, nil +} + +func (p *parser) readPythonMultilines(line string, bufferSize int) (string, error) { + parserBufferPeekResult, _ := p.buf.Peek(bufferSize) + peekBuffer := bytes.NewBuffer(parserBufferPeekResult) + + indentSize := 0 + for { + peekData, peekErr := peekBuffer.ReadBytes('\n') + if peekErr != nil { + if peekErr == io.EOF { + p.debug("readPythonMultilines: io.EOF, peekData: %q, line: %q", string(peekData), line) + return line, nil + } + + p.debug("readPythonMultilines: failed to peek with error: %v", peekErr) + return "", peekErr + } + + p.debug("readPythonMultilines: parsing %q", string(peekData)) + + peekMatches := pythonMultiline.FindStringSubmatch(string(peekData)) + p.debug("readPythonMultilines: matched %d parts", len(peekMatches)) + for n, v := range peekMatches { + p.debug(" %d: %q", n, v) + } + + // Return if not a Python multiline value. + if len(peekMatches) != 3 { + p.debug("readPythonMultilines: end of value, got: %q", line) + return line, nil + } + + // Determine indent size and line prefix. + currentIndentSize := len(peekMatches[1]) + if indentSize < 1 { + indentSize = currentIndentSize + p.debug("readPythonMultilines: indent size is %d", indentSize) + } + + // Make sure each line is indented at least as far as first line. + if currentIndentSize < indentSize { + p.debug("readPythonMultilines: end of value, current indent: %d, expected indent: %d, line: %q", currentIndentSize, indentSize, line) + return line, nil + } + + // Advance the parser reader (buffer) in-sync with the peek buffer. + _, err := p.buf.Discard(len(peekData)) + if err != nil { + p.debug("readPythonMultilines: failed to skip to the end, returning error") + return "", err + } + + // Handle indented empty line. + line += "\n" + peekMatches[1][indentSize:] + peekMatches[2] + } +} + +// parse parses data through an io.Reader. +func (f *File) parse(reader io.Reader) (err error) { + p := newParser(reader, parserOptions{ + IgnoreContinuation: f.options.IgnoreContinuation, + IgnoreInlineComment: f.options.IgnoreInlineComment, + AllowPythonMultilineValues: f.options.AllowPythonMultilineValues, + SpaceBeforeInlineComment: f.options.SpaceBeforeInlineComment, + UnescapeValueDoubleQuotes: f.options.UnescapeValueDoubleQuotes, + UnescapeValueCommentSymbols: f.options.UnescapeValueCommentSymbols, + PreserveSurroundedQuote: f.options.PreserveSurroundedQuote, + DebugFunc: f.options.DebugFunc, + ReaderBufferSize: f.options.ReaderBufferSize, + }) + if err = p.BOM(); err != nil { + return fmt.Errorf("BOM: %v", err) + } + + // Ignore error because default section name is never empty string. + name := DefaultSection + if f.options.Insensitive { + name = strings.ToLower(DefaultSection) + } + section, _ := f.NewSection(name) + + // This "last" is not strictly equivalent to "previous one" if current key is not the first nested key + var isLastValueEmpty bool + var lastRegularKey *Key + + var line []byte + var inUnparseableSection bool + + // NOTE: Iterate and increase `currentPeekSize` until + // the size of the parser buffer is found. + // TODO(unknwon): When Golang 1.10 is the lowest version supported, replace with `parserBufferSize := p.buf.Size()`. + parserBufferSize := 0 + // NOTE: Peek 4kb at a time. + currentPeekSize := minReaderBufferSize + + if f.options.AllowPythonMultilineValues { + for { + peekBytes, _ := p.buf.Peek(currentPeekSize) + peekBytesLength := len(peekBytes) + + if parserBufferSize >= peekBytesLength { + break + } + + currentPeekSize *= 2 + parserBufferSize = peekBytesLength + } + } + + for !p.isEOF { + line, err = p.readUntil('\n') + if err != nil { + return err + } + + if f.options.AllowNestedValues && + isLastValueEmpty && len(line) > 0 { + if line[0] == ' ' || line[0] == '\t' { + lastRegularKey.addNestedValue(string(bytes.TrimSpace(line))) + continue + } + } + + line = bytes.TrimLeftFunc(line, unicode.IsSpace) + if len(line) == 0 { + continue + } + + // Comments + if line[0] == '#' || line[0] == ';' { + // Note: we do not care ending line break, + // it is needed for adding second line, + // so just clean it once at the end when set to value. + p.comment.Write(line) + continue + } + + // Section + if line[0] == '[' { + // Read to the next ']' (TODO: support quoted strings) + closeIdx := bytes.LastIndexByte(line, ']') + if closeIdx == -1 { + return fmt.Errorf("unclosed section: %s", line) + } + + name := string(line[1:closeIdx]) + section, err = f.NewSection(name) + if err != nil { + return err + } + + comment, has := cleanComment(line[closeIdx+1:]) + if has { + p.comment.Write(comment) + } + + section.Comment = strings.TrimSpace(p.comment.String()) + + // Reset aotu-counter and comments + p.comment.Reset() + p.count = 1 + + inUnparseableSection = false + for i := range f.options.UnparseableSections { + if f.options.UnparseableSections[i] == name || + (f.options.Insensitive && strings.ToLower(f.options.UnparseableSections[i]) == strings.ToLower(name)) { + inUnparseableSection = true + continue + } + } + continue + } + + if inUnparseableSection { + section.isRawSection = true + section.rawBody += string(line) + continue + } + + kname, offset, err := readKeyName(f.options.KeyValueDelimiters, line) + if err != nil { + // Treat as boolean key when desired, and whole line is key name. + if IsErrDelimiterNotFound(err) { + switch { + case f.options.AllowBooleanKeys: + kname, err := p.readValue(line, parserBufferSize) + if err != nil { + return err + } + key, err := section.NewBooleanKey(kname) + if err != nil { + return err + } + key.Comment = strings.TrimSpace(p.comment.String()) + p.comment.Reset() + continue + + case f.options.SkipUnrecognizableLines: + continue + } + } + return err + } + + // Auto increment. + isAutoIncr := false + if kname == "-" { + isAutoIncr = true + kname = "#" + strconv.Itoa(p.count) + p.count++ + } + + value, err := p.readValue(line[offset:], parserBufferSize) + if err != nil { + return err + } + isLastValueEmpty = len(value) == 0 + + key, err := section.NewKey(kname, value) + if err != nil { + return err + } + key.isAutoIncrement = isAutoIncr + key.Comment = strings.TrimSpace(p.comment.String()) + p.comment.Reset() + lastRegularKey = key + } + return nil +} diff --git a/vendor/gopkg.in/ini.v1/section.go b/vendor/gopkg.in/ini.v1/section.go new file mode 100644 index 000000000000..0bd3e1301574 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/section.go @@ -0,0 +1,256 @@ +// Copyright 2014 Unknwon +// +// 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 ini + +import ( + "errors" + "fmt" + "strings" +) + +// Section represents a config section. +type Section struct { + f *File + Comment string + name string + keys map[string]*Key + keyList []string + keysHash map[string]string + + isRawSection bool + rawBody string +} + +func newSection(f *File, name string) *Section { + return &Section{ + f: f, + name: name, + keys: make(map[string]*Key), + keyList: make([]string, 0, 10), + keysHash: make(map[string]string), + } +} + +// Name returns name of Section. +func (s *Section) Name() string { + return s.name +} + +// Body returns rawBody of Section if the section was marked as unparseable. +// It still follows the other rules of the INI format surrounding leading/trailing whitespace. +func (s *Section) Body() string { + return strings.TrimSpace(s.rawBody) +} + +// SetBody updates body content only if section is raw. +func (s *Section) SetBody(body string) { + if !s.isRawSection { + return + } + s.rawBody = body +} + +// NewKey creates a new key to given section. +func (s *Section) NewKey(name, val string) (*Key, error) { + if len(name) == 0 { + return nil, errors.New("error creating new key: empty key name") + } else if s.f.options.Insensitive { + name = strings.ToLower(name) + } + + if s.f.BlockMode { + s.f.lock.Lock() + defer s.f.lock.Unlock() + } + + if inSlice(name, s.keyList) { + if s.f.options.AllowShadows { + if err := s.keys[name].addShadow(val); err != nil { + return nil, err + } + } else { + s.keys[name].value = val + s.keysHash[name] = val + } + return s.keys[name], nil + } + + s.keyList = append(s.keyList, name) + s.keys[name] = newKey(s, name, val) + s.keysHash[name] = val + return s.keys[name], nil +} + +// NewBooleanKey creates a new boolean type key to given section. +func (s *Section) NewBooleanKey(name string) (*Key, error) { + key, err := s.NewKey(name, "true") + if err != nil { + return nil, err + } + + key.isBooleanType = true + return key, nil +} + +// GetKey returns key in section by given name. +func (s *Section) GetKey(name string) (*Key, error) { + if s.f.BlockMode { + s.f.lock.RLock() + } + if s.f.options.Insensitive { + name = strings.ToLower(name) + } + key := s.keys[name] + if s.f.BlockMode { + s.f.lock.RUnlock() + } + + if key == nil { + // Check if it is a child-section. + sname := s.name + for { + if i := strings.LastIndex(sname, "."); i > -1 { + sname = sname[:i] + sec, err := s.f.GetSection(sname) + if err != nil { + continue + } + return sec.GetKey(name) + } + break + } + return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name) + } + return key, nil +} + +// HasKey returns true if section contains a key with given name. +func (s *Section) HasKey(name string) bool { + key, _ := s.GetKey(name) + return key != nil +} + +// Deprecated: Use "HasKey" instead. +func (s *Section) Haskey(name string) bool { + return s.HasKey(name) +} + +// HasValue returns true if section contains given raw value. +func (s *Section) HasValue(value string) bool { + if s.f.BlockMode { + s.f.lock.RLock() + defer s.f.lock.RUnlock() + } + + for _, k := range s.keys { + if value == k.value { + return true + } + } + return false +} + +// Key assumes named Key exists in section and returns a zero-value when not. +func (s *Section) Key(name string) *Key { + key, err := s.GetKey(name) + if err != nil { + // It's OK here because the only possible error is empty key name, + // but if it's empty, this piece of code won't be executed. + key, _ = s.NewKey(name, "") + return key + } + return key +} + +// Keys returns list of keys of section. +func (s *Section) Keys() []*Key { + keys := make([]*Key, len(s.keyList)) + for i := range s.keyList { + keys[i] = s.Key(s.keyList[i]) + } + return keys +} + +// ParentKeys returns list of keys of parent section. +func (s *Section) ParentKeys() []*Key { + var parentKeys []*Key + sname := s.name + for { + if i := strings.LastIndex(sname, "."); i > -1 { + sname = sname[:i] + sec, err := s.f.GetSection(sname) + if err != nil { + continue + } + parentKeys = append(parentKeys, sec.Keys()...) + } else { + break + } + + } + return parentKeys +} + +// KeyStrings returns list of key names of section. +func (s *Section) KeyStrings() []string { + list := make([]string, len(s.keyList)) + copy(list, s.keyList) + return list +} + +// KeysHash returns keys hash consisting of names and values. +func (s *Section) KeysHash() map[string]string { + if s.f.BlockMode { + s.f.lock.RLock() + defer s.f.lock.RUnlock() + } + + hash := map[string]string{} + for key, value := range s.keysHash { + hash[key] = value + } + return hash +} + +// DeleteKey deletes a key from section. +func (s *Section) DeleteKey(name string) { + if s.f.BlockMode { + s.f.lock.Lock() + defer s.f.lock.Unlock() + } + + for i, k := range s.keyList { + if k == name { + s.keyList = append(s.keyList[:i], s.keyList[i+1:]...) + delete(s.keys, name) + delete(s.keysHash, name) + return + } + } +} + +// ChildSections returns a list of child sections of current section. +// For example, "[parent.child1]" and "[parent.child12]" are child sections +// of section "[parent]". +func (s *Section) ChildSections() []*Section { + prefix := s.name + "." + children := make([]*Section, 0, 3) + for _, name := range s.f.sectionList { + if strings.HasPrefix(name, prefix) { + children = append(children, s.f.sections[name]) + } + } + return children +} diff --git a/vendor/gopkg.in/ini.v1/struct.go b/vendor/gopkg.in/ini.v1/struct.go new file mode 100644 index 000000000000..6bc70e4d4f8b --- /dev/null +++ b/vendor/gopkg.in/ini.v1/struct.go @@ -0,0 +1,603 @@ +// Copyright 2014 Unknwon +// +// 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 ini + +import ( + "bytes" + "errors" + "fmt" + "reflect" + "strings" + "time" + "unicode" +) + +// NameMapper represents a ini tag name mapper. +type NameMapper func(string) string + +// Built-in name getters. +var ( + // SnackCase converts to format SNACK_CASE. + SnackCase NameMapper = func(raw string) string { + newstr := make([]rune, 0, len(raw)) + for i, chr := range raw { + if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { + if i > 0 { + newstr = append(newstr, '_') + } + } + newstr = append(newstr, unicode.ToUpper(chr)) + } + return string(newstr) + } + // TitleUnderscore converts to format title_underscore. + TitleUnderscore NameMapper = func(raw string) string { + newstr := make([]rune, 0, len(raw)) + for i, chr := range raw { + if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { + if i > 0 { + newstr = append(newstr, '_') + } + chr -= 'A' - 'a' + } + newstr = append(newstr, chr) + } + return string(newstr) + } +) + +func (s *Section) parseFieldName(raw, actual string) string { + if len(actual) > 0 { + return actual + } + if s.f.NameMapper != nil { + return s.f.NameMapper(raw) + } + return raw +} + +func parseDelim(actual string) string { + if len(actual) > 0 { + return actual + } + return "," +} + +var reflectTime = reflect.TypeOf(time.Now()).Kind() + +// setSliceWithProperType sets proper values to slice based on its type. +func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { + var strs []string + if allowShadow { + strs = key.StringsWithShadows(delim) + } else { + strs = key.Strings(delim) + } + + numVals := len(strs) + if numVals == 0 { + return nil + } + + var vals interface{} + var err error + + sliceOf := field.Type().Elem().Kind() + switch sliceOf { + case reflect.String: + vals = strs + case reflect.Int: + vals, err = key.parseInts(strs, true, false) + case reflect.Int64: + vals, err = key.parseInt64s(strs, true, false) + case reflect.Uint: + vals, err = key.parseUints(strs, true, false) + case reflect.Uint64: + vals, err = key.parseUint64s(strs, true, false) + case reflect.Float64: + vals, err = key.parseFloat64s(strs, true, false) + case reflect.Bool: + vals, err = key.parseBools(strs, true, false) + case reflectTime: + vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false) + default: + return fmt.Errorf("unsupported type '[]%s'", sliceOf) + } + if err != nil && isStrict { + return err + } + + slice := reflect.MakeSlice(field.Type(), numVals, numVals) + for i := 0; i < numVals; i++ { + switch sliceOf { + case reflect.String: + slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i])) + case reflect.Int: + slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i])) + case reflect.Int64: + slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i])) + case reflect.Uint: + slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i])) + case reflect.Uint64: + slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i])) + case reflect.Float64: + slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i])) + case reflect.Bool: + slice.Index(i).Set(reflect.ValueOf(vals.([]bool)[i])) + case reflectTime: + slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i])) + } + } + field.Set(slice) + return nil +} + +func wrapStrictError(err error, isStrict bool) error { + if isStrict { + return err + } + return nil +} + +// setWithProperType sets proper value to field based on its type, +// but it does not return error for failing parsing, +// because we want to use default value that is already assigned to struct. +func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { + vt := t + isPtr := t.Kind() == reflect.Ptr + if isPtr { + vt = t.Elem() + } + switch vt.Kind() { + case reflect.String: + stringVal := key.String() + if isPtr { + field.Set(reflect.ValueOf(&stringVal)) + } else if len(stringVal) > 0 { + field.SetString(key.String()) + } + case reflect.Bool: + boolVal, err := key.Bool() + if err != nil { + return wrapStrictError(err, isStrict) + } + if isPtr { + field.Set(reflect.ValueOf(&boolVal)) + } else { + field.SetBool(boolVal) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + // ParseDuration will not return err for `0`, so check the type name + if vt.Name() == "Duration" { + durationVal, err := key.Duration() + if err != nil { + return wrapStrictError(err, isStrict) + } + if isPtr { + field.Set(reflect.ValueOf(&durationVal)) + } else if int64(durationVal) > 0 { + field.Set(reflect.ValueOf(durationVal)) + } + return nil + } + + intVal, err := key.Int64() + if err != nil { + return wrapStrictError(err, isStrict) + } + if isPtr { + pv := reflect.New(t.Elem()) + pv.Elem().SetInt(intVal) + field.Set(pv) + } else { + field.SetInt(intVal) + } + // byte is an alias for uint8, so supporting uint8 breaks support for byte + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + durationVal, err := key.Duration() + // Skip zero value + if err == nil && uint64(durationVal) > 0 { + if isPtr { + field.Set(reflect.ValueOf(&durationVal)) + } else { + field.Set(reflect.ValueOf(durationVal)) + } + return nil + } + + uintVal, err := key.Uint64() + if err != nil { + return wrapStrictError(err, isStrict) + } + if isPtr { + pv := reflect.New(t.Elem()) + pv.Elem().SetUint(uintVal) + field.Set(pv) + } else { + field.SetUint(uintVal) + } + + case reflect.Float32, reflect.Float64: + floatVal, err := key.Float64() + if err != nil { + return wrapStrictError(err, isStrict) + } + if isPtr { + pv := reflect.New(t.Elem()) + pv.Elem().SetFloat(floatVal) + field.Set(pv) + } else { + field.SetFloat(floatVal) + } + case reflectTime: + timeVal, err := key.Time() + if err != nil { + return wrapStrictError(err, isStrict) + } + if isPtr { + field.Set(reflect.ValueOf(&timeVal)) + } else { + field.Set(reflect.ValueOf(timeVal)) + } + case reflect.Slice: + return setSliceWithProperType(key, field, delim, allowShadow, isStrict) + default: + return fmt.Errorf("unsupported type '%s'", t) + } + return nil +} + +func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool) { + opts := strings.SplitN(tag, ",", 3) + rawName = opts[0] + if len(opts) > 1 { + omitEmpty = opts[1] == "omitempty" + } + if len(opts) > 2 { + allowShadow = opts[2] == "allowshadow" + } + return rawName, omitEmpty, allowShadow +} + +func (s *Section) mapTo(val reflect.Value, isStrict bool) error { + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + typ := val.Type() + + for i := 0; i < typ.NumField(); i++ { + field := val.Field(i) + tpField := typ.Field(i) + + tag := tpField.Tag.Get("ini") + if tag == "-" { + continue + } + + rawName, _, allowShadow := parseTagOptions(tag) + fieldName := s.parseFieldName(tpField.Name, rawName) + if len(fieldName) == 0 || !field.CanSet() { + continue + } + + isStruct := tpField.Type.Kind() == reflect.Struct + isStructPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct + isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous + if isAnonymous { + field.Set(reflect.New(tpField.Type.Elem())) + } + + if isAnonymous || isStruct || isStructPtr { + if sec, err := s.f.GetSection(fieldName); err == nil { + // Only set the field to non-nil struct value if we have + // a section for it. Otherwise, we end up with a non-nil + // struct ptr even though there is no data. + if isStructPtr && field.IsNil() { + field.Set(reflect.New(tpField.Type.Elem())) + } + if err = sec.mapTo(field, isStrict); err != nil { + return fmt.Errorf("error mapping field(%s): %v", fieldName, err) + } + continue + } + } + if key, err := s.GetKey(fieldName); err == nil { + delim := parseDelim(tpField.Tag.Get("delim")) + if err = setWithProperType(tpField.Type, key, field, delim, allowShadow, isStrict); err != nil { + return fmt.Errorf("error mapping field(%s): %v", fieldName, err) + } + } + } + return nil +} + +// MapTo maps section to given struct. +func (s *Section) MapTo(v interface{}) error { + typ := reflect.TypeOf(v) + val := reflect.ValueOf(v) + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } else { + return errors.New("cannot map to non-pointer struct") + } + + return s.mapTo(val, false) +} + +// StrictMapTo maps section to given struct in strict mode, +// which returns all possible error including value parsing error. +func (s *Section) StrictMapTo(v interface{}) error { + typ := reflect.TypeOf(v) + val := reflect.ValueOf(v) + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } else { + return errors.New("cannot map to non-pointer struct") + } + + return s.mapTo(val, true) +} + +// MapTo maps file to given struct. +func (f *File) MapTo(v interface{}) error { + return f.Section("").MapTo(v) +} + +// StrictMapTo maps file to given struct in strict mode, +// which returns all possible error including value parsing error. +func (f *File) StrictMapTo(v interface{}) error { + return f.Section("").StrictMapTo(v) +} + +// MapToWithMapper maps data sources to given struct with name mapper. +func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { + cfg, err := Load(source, others...) + if err != nil { + return err + } + cfg.NameMapper = mapper + return cfg.MapTo(v) +} + +// StrictMapToWithMapper maps data sources to given struct with name mapper in strict mode, +// which returns all possible error including value parsing error. +func StrictMapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { + cfg, err := Load(source, others...) + if err != nil { + return err + } + cfg.NameMapper = mapper + return cfg.StrictMapTo(v) +} + +// MapTo maps data sources to given struct. +func MapTo(v, source interface{}, others ...interface{}) error { + return MapToWithMapper(v, nil, source, others...) +} + +// StrictMapTo maps data sources to given struct in strict mode, +// which returns all possible error including value parsing error. +func StrictMapTo(v, source interface{}, others ...interface{}) error { + return StrictMapToWithMapper(v, nil, source, others...) +} + +// reflectSliceWithProperType does the opposite thing as setSliceWithProperType. +func reflectSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow bool) error { + slice := field.Slice(0, field.Len()) + if field.Len() == 0 { + return nil + } + sliceOf := field.Type().Elem().Kind() + + if allowShadow { + var keyWithShadows *Key + for i := 0; i < field.Len(); i++ { + var val string + switch sliceOf { + case reflect.String: + val = slice.Index(i).String() + case reflect.Int, reflect.Int64: + val = fmt.Sprint(slice.Index(i).Int()) + case reflect.Uint, reflect.Uint64: + val = fmt.Sprint(slice.Index(i).Uint()) + case reflect.Float64: + val = fmt.Sprint(slice.Index(i).Float()) + case reflect.Bool: + val = fmt.Sprint(slice.Index(i).Bool()) + case reflectTime: + val = slice.Index(i).Interface().(time.Time).Format(time.RFC3339) + default: + return fmt.Errorf("unsupported type '[]%s'", sliceOf) + } + + if i == 0 { + keyWithShadows = newKey(key.s, key.name, val) + } else { + keyWithShadows.AddShadow(val) + } + } + key = keyWithShadows + return nil + } + + var buf bytes.Buffer + for i := 0; i < field.Len(); i++ { + switch sliceOf { + case reflect.String: + buf.WriteString(slice.Index(i).String()) + case reflect.Int, reflect.Int64: + buf.WriteString(fmt.Sprint(slice.Index(i).Int())) + case reflect.Uint, reflect.Uint64: + buf.WriteString(fmt.Sprint(slice.Index(i).Uint())) + case reflect.Float64: + buf.WriteString(fmt.Sprint(slice.Index(i).Float())) + case reflect.Bool: + buf.WriteString(fmt.Sprint(slice.Index(i).Bool())) + case reflectTime: + buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339)) + default: + return fmt.Errorf("unsupported type '[]%s'", sliceOf) + } + buf.WriteString(delim) + } + key.SetValue(buf.String()[:buf.Len()-len(delim)]) + return nil +} + +// reflectWithProperType does the opposite thing as setWithProperType. +func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow bool) error { + switch t.Kind() { + case reflect.String: + key.SetValue(field.String()) + case reflect.Bool: + key.SetValue(fmt.Sprint(field.Bool())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + key.SetValue(fmt.Sprint(field.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + key.SetValue(fmt.Sprint(field.Uint())) + case reflect.Float32, reflect.Float64: + key.SetValue(fmt.Sprint(field.Float())) + case reflectTime: + key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339))) + case reflect.Slice: + return reflectSliceWithProperType(key, field, delim, allowShadow) + case reflect.Ptr: + if !field.IsNil() { + return reflectWithProperType(t.Elem(), key, field.Elem(), delim, allowShadow) + } + default: + return fmt.Errorf("unsupported type '%s'", t) + } + return nil +} + +// CR: copied from encoding/json/encode.go with modifications of time.Time support. +// TODO: add more test coverage. +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflectTime: + t, ok := v.Interface().(time.Time) + return ok && t.IsZero() + } + return false +} + +func (s *Section) reflectFrom(val reflect.Value) error { + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + typ := val.Type() + + for i := 0; i < typ.NumField(); i++ { + field := val.Field(i) + tpField := typ.Field(i) + + tag := tpField.Tag.Get("ini") + if tag == "-" { + continue + } + + rawName, omitEmpty, allowShadow := parseTagOptions(tag) + if omitEmpty && isEmptyValue(field) { + continue + } + + fieldName := s.parseFieldName(tpField.Name, rawName) + if len(fieldName) == 0 || !field.CanSet() { + continue + } + + if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) || + (tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") { + // Note: The only error here is section doesn't exist. + sec, err := s.f.GetSection(fieldName) + if err != nil { + // Note: fieldName can never be empty here, ignore error. + sec, _ = s.f.NewSection(fieldName) + } + + // Add comment from comment tag + if len(sec.Comment) == 0 { + sec.Comment = tpField.Tag.Get("comment") + } + + if err = sec.reflectFrom(field); err != nil { + return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) + } + continue + } + + // Note: Same reason as secion. + key, err := s.GetKey(fieldName) + if err != nil { + key, _ = s.NewKey(fieldName, "") + } + + // Add comment from comment tag + if len(key.Comment) == 0 { + key.Comment = tpField.Tag.Get("comment") + } + + if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim")), allowShadow); err != nil { + return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) + } + + } + return nil +} + +// ReflectFrom reflects secion from given struct. +func (s *Section) ReflectFrom(v interface{}) error { + typ := reflect.TypeOf(v) + val := reflect.ValueOf(v) + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } else { + return errors.New("cannot reflect from non-pointer struct") + } + + return s.reflectFrom(val) +} + +// ReflectFrom reflects file from given struct. +func (f *File) ReflectFrom(v interface{}) error { + return f.Section("").ReflectFrom(v) +} + +// ReflectFromWithMapper reflects data sources from given struct with name mapper. +func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error { + cfg.NameMapper = mapper + return cfg.ReflectFrom(v) +} + +// ReflectFrom reflects data sources from given struct. +func ReflectFrom(cfg *File, v interface{}) error { + return ReflectFromWithMapper(cfg, v, nil) +} diff --git a/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go b/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go new file mode 100644 index 000000000000..82a473bb146f --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go @@ -0,0 +1,127 @@ +/* +Copyright 2015 The Kubernetes Authors. + +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 rand provides utilities related to randomization. +package rand + +import ( + "math/rand" + "sync" + "time" +) + +var rng = struct { + sync.Mutex + rand *rand.Rand +}{ + rand: rand.New(rand.NewSource(time.Now().UnixNano())), +} + +// Int returns a non-negative pseudo-random int. +func Int() int { + rng.Lock() + defer rng.Unlock() + return rng.rand.Int() +} + +// Intn generates an integer in range [0,max). +// By design this should panic if input is invalid, <= 0. +func Intn(max int) int { + rng.Lock() + defer rng.Unlock() + return rng.rand.Intn(max) +} + +// IntnRange generates an integer in range [min,max). +// By design this should panic if input is invalid, <= 0. +func IntnRange(min, max int) int { + rng.Lock() + defer rng.Unlock() + return rng.rand.Intn(max-min) + min +} + +// IntnRange generates an int64 integer in range [min,max). +// By design this should panic if input is invalid, <= 0. +func Int63nRange(min, max int64) int64 { + rng.Lock() + defer rng.Unlock() + return rng.rand.Int63n(max-min) + min +} + +// Seed seeds the rng with the provided seed. +func Seed(seed int64) { + rng.Lock() + defer rng.Unlock() + + rng.rand = rand.New(rand.NewSource(seed)) +} + +// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n) +// from the default Source. +func Perm(n int) []int { + rng.Lock() + defer rng.Unlock() + return rng.rand.Perm(n) +} + +const ( + // We omit vowels from the set of available characters to reduce the chances + // of "bad words" being formed. + alphanums = "bcdfghjklmnpqrstvwxz2456789" + // No. of bits required to index into alphanums string. + alphanumsIdxBits = 5 + // Mask used to extract last alphanumsIdxBits of an int. + alphanumsIdxMask = 1<>= alphanumsIdxBits + remaining-- + } + return string(b) +} + +// SafeEncodeString encodes s using the same characters as rand.String. This reduces the chances of bad words and +// ensures that strings generated from hash functions appear consistent throughout the API. +func SafeEncodeString(s string) string { + r := make([]byte, len(s)) + for i, b := range []rune(s) { + r[i] = alphanums[(int(b) % len(alphanums))] + } + return string(r) +} diff --git a/vendor/k8s.io/client-go/util/retry/OWNERS b/vendor/k8s.io/client-go/util/retry/OWNERS new file mode 100644 index 000000000000..dec3e88d6318 --- /dev/null +++ b/vendor/k8s.io/client-go/util/retry/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +reviewers: +- caesarxuchao diff --git a/vendor/k8s.io/client-go/util/retry/util.go b/vendor/k8s.io/client-go/util/retry/util.go new file mode 100644 index 000000000000..15e2722f304b --- /dev/null +++ b/vendor/k8s.io/client-go/util/retry/util.go @@ -0,0 +1,105 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 retry + +import ( + "time" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" +) + +// DefaultRetry is the recommended retry for a conflict where multiple clients +// are making changes to the same resource. +var DefaultRetry = wait.Backoff{ + Steps: 5, + Duration: 10 * time.Millisecond, + Factor: 1.0, + Jitter: 0.1, +} + +// DefaultBackoff is the recommended backoff for a conflict where a client +// may be attempting to make an unrelated modification to a resource under +// active management by one or more controllers. +var DefaultBackoff = wait.Backoff{ + Steps: 4, + Duration: 10 * time.Millisecond, + Factor: 5.0, + Jitter: 0.1, +} + +// OnError allows the caller to retry fn in case the error returned by fn is retriable +// according to the provided function. backoff defines the maximum retries and the wait +// interval between two retries. +func OnError(backoff wait.Backoff, retriable func(error) bool, fn func() error) error { + var lastErr error + err := wait.ExponentialBackoff(backoff, func() (bool, error) { + err := fn() + switch { + case err == nil: + return true, nil + case retriable(err): + lastErr = err + return false, nil + default: + return false, err + } + }) + if err == wait.ErrWaitTimeout { + err = lastErr + } + return err +} + +// RetryOnConflict is used to make an update to a resource when you have to worry about +// conflicts caused by other code making unrelated updates to the resource at the same +// time. fn should fetch the resource to be modified, make appropriate changes to it, try +// to update it, and return (unmodified) the error from the update function. On a +// successful update, RetryOnConflict will return nil. If the update function returns a +// "Conflict" error, RetryOnConflict will wait some amount of time as described by +// backoff, and then try again. On a non-"Conflict" error, or if it retries too many times +// and gives up, RetryOnConflict will return an error to the caller. +// +// err := retry.RetryOnConflict(retry.DefaultRetry, func() error { +// // Fetch the resource here; you need to refetch it on every try, since +// // if you got a conflict on the last update attempt then you need to get +// // the current version before making your own changes. +// pod, err := c.Pods("mynamespace").Get(name, metav1.GetOptions{}) +// if err ! nil { +// return err +// } +// +// // Make whatever updates to the resource are needed +// pod.Status.Phase = v1.PodFailed +// +// // Try to update +// _, err = c.Pods("mynamespace").UpdateStatus(pod) +// // You have to return err itself here (not wrapped inside another error) +// // so that RetryOnConflict can identify it correctly. +// return err +// }) +// if err != nil { +// // May be conflict if max retries were hit, or may be something unrelated +// // like permissions or a network error +// return err +// } +// ... +// +// TODO: Make Backoff an interface? +func RetryOnConflict(backoff wait.Backoff, fn func() error) error { + return OnError(backoff, errors.IsConflict, fn) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 833a7b81e9e3..bba83055226b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -313,6 +313,9 @@ google.golang.org/protobuf/types/known/durationpb google.golang.org/protobuf/types/known/timestamppb # gopkg.in/inf.v0 v0.9.1 gopkg.in/inf.v0 +# gopkg.in/ini.v1 v1.51.0 +## explicit +gopkg.in/ini.v1 # gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 # gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 @@ -407,6 +410,7 @@ k8s.io/apimachinery/pkg/util/json k8s.io/apimachinery/pkg/util/mergepatch k8s.io/apimachinery/pkg/util/naming k8s.io/apimachinery/pkg/util/net +k8s.io/apimachinery/pkg/util/rand k8s.io/apimachinery/pkg/util/runtime k8s.io/apimachinery/pkg/util/sets k8s.io/apimachinery/pkg/util/strategicpatch @@ -644,6 +648,7 @@ k8s.io/client-go/util/flowcontrol k8s.io/client-go/util/homedir k8s.io/client-go/util/jsonpath k8s.io/client-go/util/keyutil +k8s.io/client-go/util/retry k8s.io/client-go/util/workqueue # k8s.io/component-base v0.20.2 k8s.io/component-base/config From c8e361754d681085642ba900acf7804f89849878 Mon Sep 17 00:00:00 2001 From: Cesar Wong Date: Tue, 2 Mar 2021 11:36:24 -0500 Subject: [PATCH 2/3] Add destroy infra and destroy iam commands --- api/fixtures/example.go | 1 + cmd/destroy/destroy.go | 19 ++ cmd/infra/aws/create.go | 15 +- cmd/infra/aws/create_iam.go | 9 +- cmd/infra/aws/destroy.go | 383 +++++++++++++++++++++++++++++++++++ cmd/infra/aws/destroy_iam.go | 108 ++++++++++ cmd/infra/aws/ec2.go | 21 +- cmd/infra/aws/iam.go | 12 +- cmd/infra/destroy.go | 18 ++ cmd/infra/destroy_iam.go | 18 ++ main.go | 2 + 11 files changed, 589 insertions(+), 17 deletions(-) create mode 100644 cmd/destroy/destroy.go create mode 100644 cmd/infra/aws/destroy.go create mode 100644 cmd/infra/aws/destroy_iam.go create mode 100644 cmd/infra/destroy.go create mode 100644 cmd/infra/destroy_iam.go diff --git a/api/fixtures/example.go b/api/fixtures/example.go index 96535149dc1e..59e45fbe8bb4 100644 --- a/api/fixtures/example.go +++ b/api/fixtures/example.go @@ -130,6 +130,7 @@ func (o ExampleOptions) Resources() *ExampleResources { Platform: hyperv1.PlatformSpec{ AWS: &hyperv1.AWSPlatformSpec{ Region: o.AWS.Region, + VPC: o.AWS.VPCID, NodePoolDefaults: &hyperv1.AWSNodePoolPlatform{ InstanceType: o.AWS.InstanceType, InstanceProfile: o.AWS.InstanceProfile, diff --git a/cmd/destroy/destroy.go b/cmd/destroy/destroy.go new file mode 100644 index 000000000000..3f26b3dde1fd --- /dev/null +++ b/cmd/destroy/destroy.go @@ -0,0 +1,19 @@ +package create + +import ( + "github.com/spf13/cobra" + + "github.com/openshift/hypershift/cmd/infra" +) + +func NewCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "destroy", + Short: "Commands for destroying HyperShift resources", + } + + cmd.AddCommand(infra.NewDestroyCommand()) + cmd.AddCommand(infra.NewDestroyIAMCommand()) + + return cmd +} diff --git a/cmd/infra/aws/create.go b/cmd/infra/aws/create.go index a3eea67e134b..71478eeef823 100644 --- a/cmd/infra/aws/create.go +++ b/cmd/infra/aws/create.go @@ -18,6 +18,9 @@ type CreateInfraOptions struct { InfraID string AWSCredentialsFile string OutputFile string + AdditionalTags []string + + additionalEC2Tags []*ec2.Tag } type CreateInfraOutput struct { @@ -53,6 +56,7 @@ func NewCreateCommand() *cobra.Command { cmd.Flags().StringVar(&opts.AWSCredentialsFile, "aws-creds", opts.AWSCredentialsFile, "Path to an AWS credentials file (required)") cmd.Flags().StringVar(&opts.OutputFile, "output-file", opts.OutputFile, "Path to file that will contain output information from infra resources (optional)") cmd.Flags().StringVar(&opts.Region, "region", opts.Region, "Region where cluster infra should be created") + cmd.Flags().StringSliceVar(&opts.AdditionalTags, "additional-tags", opts.AdditionalTags, "Additional tags to set on AWS resources") cmd.MarkFlagRequired("infra-id") cmd.MarkFlagRequired("aws-creds") @@ -91,12 +95,15 @@ func (o *CreateInfraOptions) Run() error { func (o *CreateInfraOptions) CreateInfra() (*CreateInfraOutput, error) { var err error + if err = o.parseAdditionalTags(); err != nil { + return nil, err + } result := &CreateInfraOutput{ InfraID: o.InfraID, ComputeCIDR: DefaultCIDRBlock, Region: o.Region, } - client, err := o.AWSClient() + client, err := AWSClient(o.AWSCredentialsFile, o.Region) if err != nil { return nil, err } @@ -146,11 +153,11 @@ func (o *CreateInfraOptions) CreateInfra() (*CreateInfraOutput, error) { return result, nil } -func (o *CreateInfraOptions) AWSClient() (ec2iface.EC2API, error) { +func AWSClient(creds, region string) (ec2iface.EC2API, error) { awsConfig := &aws.Config{ - Region: aws.String(o.Region), + Region: aws.String(region), } - awsConfig.Credentials = credentials.NewSharedCredentials(o.AWSCredentialsFile, "default") + awsConfig.Credentials = credentials.NewSharedCredentials(creds, "default") s, err := session.NewSession(awsConfig) if err != nil { return nil, fmt.Errorf("failed to create client session: %w", err) diff --git a/cmd/infra/aws/create_iam.go b/cmd/infra/aws/create_iam.go index c8c88d282585..225ed9c3e0dd 100644 --- a/cmd/infra/aws/create_iam.go +++ b/cmd/infra/aws/create_iam.go @@ -32,7 +32,6 @@ func NewCreateIAMCommand() *cobra.Command { cmd.Flags().StringVar(&opts.ProfileName, "profile-name", opts.ProfileName, "Name of IAM instance profile to creeate") cmd.Flags().StringVar(&opts.Region, "region", opts.Region, "Region where cluster infra should be created") - cmd.MarkFlagRequired("infra-id") cmd.MarkFlagRequired("aws-creds") cmd.RunE = func(cmd *cobra.Command, args []string) error { @@ -51,18 +50,18 @@ func (o *CreateIAMOptions) Run() error { func (o *CreateIAMOptions) CreateIAM() error { var err error - client, err := o.IAMClient() + client, err := IAMClient(o.AWSCredentialsFile, o.Region) if err != nil { return err } return o.CreateWorkerInstanceProfile(client, o.ProfileName) } -func (o *CreateIAMOptions) IAMClient() (iamiface.IAMAPI, error) { +func IAMClient(creds, region string) (iamiface.IAMAPI, error) { awsConfig := &aws.Config{ - Region: aws.String(o.Region), + Region: aws.String(region), } - awsConfig.Credentials = credentials.NewSharedCredentials(o.AWSCredentialsFile, "default") + awsConfig.Credentials = credentials.NewSharedCredentials(creds, "default") s, err := session.NewSession(awsConfig) if err != nil { return nil, fmt.Errorf("failed to create client session: %w", err) diff --git a/cmd/infra/aws/destroy.go b/cmd/infra/aws/destroy.go new file mode 100644 index 000000000000..f6c766bd49ba --- /dev/null +++ b/cmd/infra/aws/destroy.go @@ -0,0 +1,383 @@ +package aws + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/spf13/cobra" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/wait" +) + +type DestroyInfraOptions struct { + Region string + InfraID string + AWSCredentialsFile string +} + +func NewDestroyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "aws", + Short: "Destroys AWS infrastructure resources for a cluster", + } + + opts := DestroyInfraOptions{ + Region: "us-east-1", + } + + cmd.Flags().StringVar(&opts.InfraID, "infra-id", opts.InfraID, "Cluster ID with which to tag AWS resources (required)") + cmd.Flags().StringVar(&opts.AWSCredentialsFile, "aws-creds", opts.AWSCredentialsFile, "Path to an AWS credentials file (required)") + cmd.Flags().StringVar(&opts.Region, "region", opts.Region, "Region where cluster infra should be created") + + cmd.MarkFlagRequired("infra-id") + cmd.MarkFlagRequired("aws-creds") + + cmd.Run = func(cmd *cobra.Command, args []string) { + opts.Run(context.Background()) + fmt.Printf("Successfully destroyed AWS infra\n") + } + + return cmd +} + +func (o *DestroyInfraOptions) Run(ctx context.Context) { + wait.PollUntil(5*time.Second, func() (bool, error) { + err := o.DestroyInfra(ctx) + if err != nil { + return false, nil + } + return true, nil + }, ctx.Done()) +} + +func (o *DestroyInfraOptions) DestroyInfra(ctx context.Context) error { + var errs []error + client, err := AWSClient(o.AWSCredentialsFile, o.Region) + if err != nil { + return err + } + errs = append(errs, o.DestroyInternetGateways(ctx, client)...) + errs = append(errs, o.DestroyVPCs(ctx, client)...) + errs = append(errs, o.DestroyDHCPOptions(ctx, client)...) + errs = append(errs, o.DestroyEIPs(ctx, client)...) + return utilerrors.NewAggregate(errs) +} + +func (o *DestroyInfraOptions) DestroyVPCEndpoints(ctx context.Context, client ec2iface.EC2API, vpcID *string) []error { + var errs []error + deleteVPCEndpoints := func(out *ec2.DescribeVpcEndpointsOutput, _ bool) bool { + ids := make([]*string, 0, len(out.VpcEndpoints)) + for _, ep := range out.VpcEndpoints { + ids = append(ids, ep.VpcEndpointId) + } + if len(ids) > 0 { + _, err := client.DeleteVpcEndpointsWithContext(ctx, &ec2.DeleteVpcEndpointsInput{ + VpcEndpointIds: ids, + }) + if err != nil { + errs = append(errs, err) + } + } + return true + } + err := client.DescribeVpcEndpointsPagesWithContext(ctx, + &ec2.DescribeVpcEndpointsInput{Filters: vpcFilter(vpcID)}, + deleteVPCEndpoints) + if err != nil { + errs = append(errs, err) + } + return errs +} + +func (o *DestroyInfraOptions) DestroyRouteTables(ctx context.Context, client ec2iface.EC2API, vpcID *string) []error { + var errs []error + deleteRouteTables := func(out *ec2.DescribeRouteTablesOutput, _ bool) bool { + for _, routeTable := range out.RouteTables { + var routeErrs []error + for _, route := range routeTable.Routes { + if aws.StringValue(route.Origin) == "CreateRoute" { + _, err := client.DeleteRouteWithContext(ctx, &ec2.DeleteRouteInput{ + RouteTableId: routeTable.RouteTableId, + DestinationCidrBlock: route.DestinationCidrBlock, + DestinationIpv6CidrBlock: route.DestinationIpv6CidrBlock, + DestinationPrefixListId: route.DestinationPrefixListId, + }) + if err != nil { + routeErrs = append(routeErrs, err) + } + } + } + if len(routeErrs) > 0 { + errs = append(errs, routeErrs...) + continue + } + hasMain := false + var assocErrs []error + for _, assoc := range routeTable.Associations { + if aws.BoolValue(assoc.Main) { + hasMain = true + continue + } + _, err := client.DisassociateRouteTableWithContext(ctx, &ec2.DisassociateRouteTableInput{ + AssociationId: assoc.RouteTableAssociationId, + }) + if err != nil { + assocErrs = append(assocErrs, err) + } + } + if len(assocErrs) > 0 { + errs = append(errs, assocErrs...) + continue + } + if hasMain { + continue + } + _, err := client.DeleteRouteTableWithContext(ctx, &ec2.DeleteRouteTableInput{ + RouteTableId: routeTable.RouteTableId, + }) + if err != nil { + errs = append(errs, err) + } + } + return false + } + + err := client.DescribeRouteTablesPagesWithContext(ctx, + &ec2.DescribeRouteTablesInput{Filters: vpcFilter(vpcID)}, + deleteRouteTables) + if err != nil { + errs = append(errs, err) + } + return errs +} + +func (o *DestroyInfraOptions) DestroySecurityGroups(ctx context.Context, client ec2iface.EC2API, vpcID *string) []error { + var errs []error + deleteSecurityGroups := func(out *ec2.DescribeSecurityGroupsOutput, _ bool) bool { + for _, sg := range out.SecurityGroups { + var permissionErrs []error + if len(sg.IpPermissions) > 0 { + _, err := client.RevokeSecurityGroupIngressWithContext(ctx, &ec2.RevokeSecurityGroupIngressInput{ + GroupId: sg.GroupId, + IpPermissions: sg.IpPermissions, + }) + if err != nil { + permissionErrs = append(permissionErrs, err) + } + } + + if len(sg.IpPermissionsEgress) > 0 { + _, err := client.RevokeSecurityGroupEgressWithContext(ctx, &ec2.RevokeSecurityGroupEgressInput{ + GroupId: sg.GroupId, + IpPermissions: sg.IpPermissionsEgress, + }) + if err != nil { + permissionErrs = append(permissionErrs, err) + } + } + if len(permissionErrs) > 0 { + errs = append(errs, permissionErrs...) + continue + } + if aws.StringValue(sg.GroupName) == "default" { + continue + } + _, err := client.DeleteSecurityGroupWithContext(ctx, &ec2.DeleteSecurityGroupInput{ + GroupId: sg.GroupId, + }) + if err != nil { + errs = append(errs, err) + } + } + + return true + } + + err := client.DescribeSecurityGroupsPagesWithContext(ctx, + &ec2.DescribeSecurityGroupsInput{Filters: vpcFilter(vpcID)}, + deleteSecurityGroups) + if err != nil { + errs = append(errs, err) + } + return errs +} + +func (o *DestroyInfraOptions) DestroyNATGateways(ctx context.Context, client ec2iface.EC2API, vpcID *string) []error { + var errs []error + deleteNATGateways := func(out *ec2.DescribeNatGatewaysOutput, _ bool) bool { + for _, natGateway := range out.NatGateways { + _, err := client.DeleteNatGatewayWithContext(ctx, &ec2.DeleteNatGatewayInput{ + NatGatewayId: natGateway.NatGatewayId, + }) + if err != nil { + errs = append(errs, err) + } + } + return true + } + err := client.DescribeNatGatewaysPagesWithContext(ctx, + &ec2.DescribeNatGatewaysInput{Filter: vpcFilter(vpcID)}, + deleteNATGateways) + if err != nil { + errs = append(errs, err) + } + return errs +} + +func (o *DestroyInfraOptions) DestroyInternetGateways(ctx context.Context, client ec2iface.EC2API) []error { + var errs []error + deleteInternetGateways := func(out *ec2.DescribeInternetGatewaysOutput, _ bool) bool { + for _, igw := range out.InternetGateways { + var detachErrs []error + for _, attachment := range igw.Attachments { + _, err := client.DetachInternetGatewayWithContext(ctx, &ec2.DetachInternetGatewayInput{ + InternetGatewayId: igw.InternetGatewayId, + VpcId: attachment.VpcId, + }) + if err != nil { + detachErrs = append(detachErrs, err) + } + } + if len(detachErrs) > 0 { + errs = append(errs, detachErrs...) + continue + } + _, err := client.DeleteInternetGatewayWithContext(ctx, &ec2.DeleteInternetGatewayInput{ + InternetGatewayId: igw.InternetGatewayId, + }) + if err != nil { + errs = append(errs, err) + } + } + return true + } + + err := client.DescribeInternetGatewaysPagesWithContext(ctx, + &ec2.DescribeInternetGatewaysInput{Filters: o.ec2Filters()}, + deleteInternetGateways) + if err != nil { + errs = append(errs, err) + } + return nil +} + +func (o *DestroyInfraOptions) DestroySubnets(ctx context.Context, client ec2iface.EC2API, vpcID *string) []error { + var errs []error + deleteSubnets := func(out *ec2.DescribeSubnetsOutput, _ bool) bool { + for _, subnet := range out.Subnets { + _, err := client.DeleteSubnetWithContext(ctx, &ec2.DeleteSubnetInput{ + SubnetId: subnet.SubnetId, + }) + if err != nil { + errs = append(errs, err) + } + } + return true + } + err := client.DescribeSubnetsPagesWithContext(ctx, + &ec2.DescribeSubnetsInput{Filters: vpcFilter(vpcID)}, + deleteSubnets) + if err != nil { + errs = append(errs, err) + } + return errs +} + +func (o *DestroyInfraOptions) DestroyVPCs(ctx context.Context, client ec2iface.EC2API) []error { + var errs []error + deleteVPC := func(out *ec2.DescribeVpcsOutput, _ bool) bool { + for _, vpc := range out.Vpcs { + var childErrs []error + childErrs = append(errs, o.DestroyVPCEndpoints(ctx, client, vpc.VpcId)...) + childErrs = append(errs, o.DestroyRouteTables(ctx, client, vpc.VpcId)...) + childErrs = append(errs, o.DestroySecurityGroups(ctx, client, vpc.VpcId)...) + childErrs = append(errs, o.DestroyNATGateways(ctx, client, vpc.VpcId)...) + childErrs = append(errs, o.DestroySubnets(ctx, client, vpc.VpcId)...) + if len(childErrs) > 0 { + errs = append(errs, childErrs...) + continue + } + _, err := client.DeleteVpcWithContext(ctx, &ec2.DeleteVpcInput{ + VpcId: vpc.VpcId, + }) + if err != nil { + errs = append(errs, err) + } + } + return true + } + err := client.DescribeVpcsPagesWithContext(ctx, + &ec2.DescribeVpcsInput{Filters: o.ec2Filters()}, + deleteVPC) + + if err != nil { + errs = append(errs, err) + } + return errs +} + +func (o *DestroyInfraOptions) DestroyDHCPOptions(ctx context.Context, client ec2iface.EC2API) []error { + var errs []error + deleteDHCPOptions := func(out *ec2.DescribeDhcpOptionsOutput, _ bool) bool { + for _, dhcpOpt := range out.DhcpOptions { + _, err := client.DeleteDhcpOptionsWithContext(ctx, &ec2.DeleteDhcpOptionsInput{ + DhcpOptionsId: dhcpOpt.DhcpOptionsId, + }) + if err != nil { + errs = append(errs, err) + } + } + return true + } + err := client.DescribeDhcpOptionsPagesWithContext(ctx, + &ec2.DescribeDhcpOptionsInput{Filters: o.ec2Filters()}, + deleteDHCPOptions) + if err != nil { + errs = append(errs, err) + } + return errs +} + +func (o *DestroyInfraOptions) DestroyEIPs(ctx context.Context, client ec2iface.EC2API) []error { + var errs []error + out, err := client.DescribeAddressesWithContext(ctx, &ec2.DescribeAddressesInput{ + Filters: o.ec2Filters(), + }) + if err != nil { + errs = append(errs, err) + return errs + } + + for _, addr := range out.Addresses { + _, err := client.ReleaseAddressWithContext(ctx, &ec2.ReleaseAddressInput{ + AllocationId: addr.AllocationId, + }) + if err != nil { + errs = append(errs, err) + } + } + return errs +} + +func (o *DestroyInfraOptions) ec2Filters() []*ec2.Filter { + return []*ec2.Filter{ + { + Name: aws.String(fmt.Sprintf("tag:%s", clusterTag(o.InfraID))), + Values: []*string{aws.String(clusterTagValue)}, + }, + } +} + +func vpcFilter(vpcID *string) []*ec2.Filter { + return []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{vpcID}, + }, + } +} diff --git a/cmd/infra/aws/destroy_iam.go b/cmd/infra/aws/destroy_iam.go new file mode 100644 index 000000000000..ca00d67ba2e6 --- /dev/null +++ b/cmd/infra/aws/destroy_iam.go @@ -0,0 +1,108 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/iam/iamiface" + "github.com/spf13/cobra" +) + +type DestroyIAMOptions struct { + Region string + AWSCredentialsFile string + ProfileName string +} + +func NewDestroyIAMCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "aws", + Short: "Destroys AWS instance profile for workers", + } + + opts := DestroyIAMOptions{ + Region: "us-east-1", + ProfileName: "hypershift-worker-profile", + } + + cmd.Flags().StringVar(&opts.AWSCredentialsFile, "aws-creds", opts.AWSCredentialsFile, "Path to an AWS credentials file (required)") + cmd.Flags().StringVar(&opts.ProfileName, "profile-name", opts.ProfileName, "Name of IAM instance profile to destroy") + cmd.Flags().StringVar(&opts.Region, "region", opts.Region, "Region where cluster infra lives") + + cmd.MarkFlagRequired("aws-creds") + + cmd.RunE = func(cmd *cobra.Command, args []string) error { + return opts.Run() + } + + return cmd +} + +func (o *DestroyIAMOptions) Run() error { + if err := o.DestroyIAM(); err != nil { + return err + } + return nil +} + +func (o *DestroyIAMOptions) DestroyIAM() error { + var err error + client, err := IAMClient(o.AWSCredentialsFile, o.Region) + if err != nil { + return err + } + return o.DestroyWorkerInstanceProfile(client, o.ProfileName) +} + +func (o *DestroyIAMOptions) DestroyWorkerInstanceProfile(client iamiface.IAMAPI, profileName string) error { + instanceProfile, err := existingInstanceProfile(client, o.ProfileName) + if err != nil { + return fmt.Errorf("cannot check for existing instance profile: %w", err) + } + if instanceProfile != nil { + for _, role := range instanceProfile.Roles { + _, err := client.RemoveRoleFromInstanceProfile(&iam.RemoveRoleFromInstanceProfileInput{ + InstanceProfileName: aws.String(o.ProfileName), + RoleName: role.RoleName, + }) + if err != nil { + return fmt.Errorf("cannot remove role %s from instance profile %s: %w", aws.StringValue(role.RoleName), o.ProfileName, err) + } + } + _, err := client.DeleteInstanceProfile(&iam.DeleteInstanceProfileInput{ + InstanceProfileName: aws.String(o.ProfileName), + }) + if err != nil { + return fmt.Errorf("cannot delete instance profile %s: %w", o.ProfileName, err) + } + } + roleName := fmt.Sprintf("%s-role", o.ProfileName) + policyName := fmt.Sprintf("%s-policy", o.ProfileName) + role, err := existingRole(client, roleName) + if err != nil { + return fmt.Errorf("cannot check for existing role: %w", err) + } + if role != nil { + hasPolicy, err := existingRolePolicy(client, roleName, policyName) + if err != nil { + return fmt.Errorf("cannot check for existing role policy: %w", err) + } + if hasPolicy { + _, err := client.DeleteRolePolicy(&iam.DeleteRolePolicyInput{ + PolicyName: aws.String(policyName), + RoleName: aws.String(roleName), + }) + if err != nil { + return fmt.Errorf("cannot delete role policy %s from role %s: %w", policyName, roleName, err) + } + } + _, err = client.DeleteRole(&iam.DeleteRoleInput{ + RoleName: aws.String(roleName), + }) + if err != nil { + return fmt.Errorf("cannot delete role %s: %w", roleName, err) + } + } + return nil +} diff --git a/cmd/infra/aws/ec2.go b/cmd/infra/aws/ec2.go index d231cba5848c..a4debf850eaa 100644 --- a/cmd/infra/aws/ec2.go +++ b/cmd/infra/aws/ec2.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "strings" "time" "github.com/aws/aws-sdk-go/aws" @@ -264,7 +265,7 @@ func (o *CreateInfraOptions) CreateNATGateway(client ec2iface.EC2API, publicSubn // recognizing the EIP as belonging to the cluster _, err = client.CreateTags(&ec2.CreateTagsInput{ Resources: []*string{aws.String(allocationID)}, - Tags: ec2Tags(o.InfraID, fmt.Sprintf("%s-eip-%s", o.InfraID, availabilityZone)), + Tags: append(ec2Tags(o.InfraID, fmt.Sprintf("%s-eip-%s", o.InfraID, availabilityZone)), o.additionalEC2Tags...), }) if err != nil { return "", fmt.Errorf("cannot tag NAT gateway EIP: %w", err) @@ -491,11 +492,27 @@ func (o *CreateInfraOptions) ec2TagSpecifications(resourceType, name string) []* return []*ec2.TagSpecification{ { ResourceType: aws.String(resourceType), - Tags: ec2Tags(o.InfraID, name), + Tags: append(ec2Tags(o.InfraID, name), o.additionalEC2Tags...), }, } } +func (o *CreateInfraOptions) parseAdditionalTags() error { + var ec2Tags []*ec2.Tag + for _, tagStr := range o.AdditionalTags { + parts := strings.SplitN(tagStr, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid tag specification: %q (expecting \"key=value\")", tagStr) + } + ec2Tags = append(ec2Tags, &ec2.Tag{ + Key: aws.String(parts[0]), + Value: aws.String(parts[1]), + }) + } + o.additionalEC2Tags = ec2Tags + return nil +} + func (o *CreateInfraOptions) ec2Filters(name string) []*ec2.Filter { filters := []*ec2.Filter{ { diff --git a/cmd/infra/aws/iam.go b/cmd/infra/aws/iam.go index 79b016a4cf13..021969d07602 100644 --- a/cmd/infra/aws/iam.go +++ b/cmd/infra/aws/iam.go @@ -39,7 +39,7 @@ func (o *CreateIAMOptions) CreateWorkerInstanceProfile(client iamiface.IAMAPI, p }` ) roleName := fmt.Sprintf("%s-role", profileName) - role, err := o.existingRole(client, roleName) + role, err := existingRole(client, roleName) if err != nil { return err } @@ -53,7 +53,7 @@ func (o *CreateIAMOptions) CreateWorkerInstanceProfile(client iamiface.IAMAPI, p return fmt.Errorf("cannot create worker role: %w", err) } } - instanceProfile, err := o.existingInstanceProfile(client, profileName) + instanceProfile, err := existingInstanceProfile(client, profileName) if err != nil { return err } @@ -83,7 +83,7 @@ func (o *CreateIAMOptions) CreateWorkerInstanceProfile(client iamiface.IAMAPI, p } } rolePolicyName := fmt.Sprintf("%s-policy", profileName) - hasPolicy, err := o.existingRolePolicy(client, roleName, rolePolicyName) + hasPolicy, err := existingRolePolicy(client, roleName, rolePolicyName) if err != nil { return err } @@ -100,7 +100,7 @@ func (o *CreateIAMOptions) CreateWorkerInstanceProfile(client iamiface.IAMAPI, p return nil } -func (o *CreateIAMOptions) existingRole(client iamiface.IAMAPI, roleName string) (*iam.Role, error) { +func existingRole(client iamiface.IAMAPI, roleName string) (*iam.Role, error) { result, err := client.GetRole(&iam.GetRoleInput{RoleName: aws.String(roleName)}) if err != nil { if awsErr, ok := err.(awserr.Error); ok { @@ -113,7 +113,7 @@ func (o *CreateIAMOptions) existingRole(client iamiface.IAMAPI, roleName string) return result.Role, nil } -func (o *CreateIAMOptions) existingInstanceProfile(client iamiface.IAMAPI, profileName string) (*iam.InstanceProfile, error) { +func existingInstanceProfile(client iamiface.IAMAPI, profileName string) (*iam.InstanceProfile, error) { result, err := client.GetInstanceProfile(&iam.GetInstanceProfileInput{ InstanceProfileName: aws.String(profileName), }) @@ -128,7 +128,7 @@ func (o *CreateIAMOptions) existingInstanceProfile(client iamiface.IAMAPI, profi return result.InstanceProfile, nil } -func (o *CreateIAMOptions) existingRolePolicy(client iamiface.IAMAPI, roleName, policyName string) (bool, error) { +func existingRolePolicy(client iamiface.IAMAPI, roleName, policyName string) (bool, error) { result, err := client.GetRolePolicy(&iam.GetRolePolicyInput{ RoleName: aws.String(roleName), PolicyName: aws.String(policyName), diff --git a/cmd/infra/destroy.go b/cmd/infra/destroy.go new file mode 100644 index 000000000000..4e2378e51e2b --- /dev/null +++ b/cmd/infra/destroy.go @@ -0,0 +1,18 @@ +package infra + +import ( + "github.com/spf13/cobra" + + "github.com/openshift/hypershift/cmd/infra/aws" +) + +func NewDestroyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "infra", + Short: "Commands for destroying HyperShift infra resources", + } + + cmd.AddCommand(aws.NewDestroyCommand()) + + return cmd +} diff --git a/cmd/infra/destroy_iam.go b/cmd/infra/destroy_iam.go new file mode 100644 index 000000000000..ea22b7abe697 --- /dev/null +++ b/cmd/infra/destroy_iam.go @@ -0,0 +1,18 @@ +package infra + +import ( + "github.com/spf13/cobra" + + "github.com/openshift/hypershift/cmd/infra/aws" +) + +func NewDestroyIAMCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "iam", + Short: "Commands for destroying HyperShift IAM resources", + } + + cmd.AddCommand(aws.NewDestroyIAMCommand()) + + return cmd +} diff --git a/main.go b/main.go index 61d1bf5aa808..fbf6d788813c 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" createcmd "github.com/openshift/hypershift/cmd/create" + destroycmd "github.com/openshift/hypershift/cmd/destroy" installcmd "github.com/openshift/hypershift/cmd/install" ) @@ -36,6 +37,7 @@ func main() { } cmd.AddCommand(installcmd.NewCommand()) cmd.AddCommand(createcmd.NewCommand()) + cmd.AddCommand(destroycmd.NewCommand()) if err := cmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) From b6f3f6dc2a7d4bcff41fd9b47dde463a5600cf4e Mon Sep 17 00:00:00 2001 From: Cesar Wong Date: Thu, 4 Mar 2021 09:47:04 -0500 Subject: [PATCH 3/3] Update documentation --- README.md | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7728cac2c468..be04ce959990 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ hypershift install --render | oc delete -f - ## How to create a hosted cluster -The `hypershift` CLI tool comes with a command to help create an example hosted cluster. The cluster will come with a node pool consisting of two workers nodes. +The `hypershift` CLI tool comes with commands to help create an example hosted cluster. The cluster will come with a node pool consisting of two workers nodes. **Prerequisites:** @@ -55,14 +55,32 @@ The `hypershift` CLI tool comes with a command to help create an example hosted - An SSH public key file for guest node access - An [AWS credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) with permissions to create infrastructure for the cluster +Run the `hypershift` command to create an IAM instance profile for your workers: +```shell +hypershift create iam aws --aws-creds /my/aws-credentials +``` +NOTE: The default profile name is `hypershift-worker-profile`. To use a different name (for example, in a shared account), use the `--profile-name` flag. The worker instance profile only needs to be created once per account and you can reuse it as needed for your clusters. + +Run the `hypershift` command to create cloud infrastructure for your cluster: +NOTE: Infrastructure for a cluster can be created once and reused. However it should only correspond to one cluster at a time. +```shell +hypershift create infra aws --aws-creds /my/aws-credentials --infra-id INFRA-ID --region us-east-2 --output-file /tmp/infra.json +``` +For `INFRA-ID` use a short identifier for your cluster such as `mycluster-1234`. It should be unique in your AWS account. +For region, the default region is `us-east-1`, specify a different region if desired. +The output file will contain JSON with the details of your provisioned infrastructure. + Run the `hypershift` command to generate and install the example cluster: ```shell hypershift create cluster \ --pull-secret /my/pull-secret \ --aws-creds /my/aws-credentials \ - --ssh-key /my/ssh-public-key + --ssh-key /my/ssh-public-key \ + --infra-json /tmp/infra.json ``` +NOTE: The file specified in the `--infra-json` flag should be the same file you created with the `create infra aws` command above. +If you created an instance profile named something other than `hypershift-worker-profile`, you need to pass the profile name with the `--instance-profile` flag. Eventually the cluster's kubeconfig will become available and can be fetched and decoded locally: @@ -79,6 +97,19 @@ To delete the cluster, run: oc delete --namespace clusters ``` +NOTE: After deleting the cluster, you can use an existing `infra.json` to create a new cluster. + +To destroy your AWS infrastructure: +```shell +hypershift destroy infra aws --aws-creds /my/aws/credentials --infra-id INFRA-ID --region us-east-2 +``` +Specify the same INFRA-ID and region as your original `create infra` command. + +To destroy the IAM instance profile: +```shell +hypershift destroy iam aws --aws-creds /my/aws-credentials +``` + ## How to add node pools to the example cluster **Prerequisites:**