├── codecov.yml ├── hack ├── tools.go ├── instance-manager.png ├── boilerplate.go.txt └── update-codegen.sh ├── config ├── rbac │ ├── kustomization.yaml │ ├── service_account.yaml │ ├── role_binding.yaml │ ├── strategy_role_binding.yaml │ ├── strategy_role.yaml │ └── role.yaml ├── crd │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── bases │ │ └── instance-manager-deployment.yaml └── default │ └── kustomization.yaml ├── docs ├── demo.gif ├── README.md ├── examples │ ├── EKS-managed.md │ └── EKS-fargate.md └── FAQ.md ├── PROJECT ├── .gitignore ├── test-bdd ├── templates │ ├── namespace.yaml │ ├── namespace-gitops.yaml │ ├── instance-group-gitops.yaml │ ├── instance-group-fargate.yaml │ ├── instance-group-managed.yaml │ ├── instance-group.yaml │ ├── instance-group-wp.yaml │ ├── instance-group-latest-locked.yaml │ ├── instance-group-launch-template.yaml │ ├── instance-group-launch-template-mixed.yaml │ ├── instance-group-crd.yaml │ ├── instance-group-crd-wp.yaml │ └── manager-configmap.yaml ├── features │ ├── 04_delete.feature │ ├── 02_update.feature │ ├── 03_upgrade.feature │ └── 01_create.feature └── testutil │ └── helpers.go ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question.md │ ├── documentation_issue.md │ ├── feature_request.md │ └── bug_report.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── workflows │ ├── unit-test.yml │ ├── codeql.yml │ ├── golangci-lint.yml │ ├── functional-test.yaml │ └── image-push.yml ├── dependabot.yml ├── CODE_OF_CONDUCT.md ├── PULL_REQUEST_TEMPLATE.md └── DEVELOPER.md ├── .golangci.yml ├── controllers ├── providers │ ├── aws │ │ ├── eks_test.go │ │ ├── retry.go │ │ ├── ssm.go │ │ ├── predicates.go │ │ └── constructors.go │ └── kubernetes │ │ ├── spot.go │ │ ├── rollingupdate.go │ │ ├── events.go │ │ └── utils_test.go ├── provisioners │ ├── eksfargate │ │ └── types.go │ ├── provisioners.go │ ├── eks │ │ ├── state.go │ │ ├── scaling │ │ │ └── interface.go │ │ ├── delete.go │ │ ├── state_test.go │ │ └── create.go │ └── eksmanaged │ │ └── types.go ├── common │ ├── utils_test.go │ ├── bootstrap.go │ └── metrics.go ├── interface.go └── reconcilers_test.go ├── client ├── clientset │ └── versioned │ │ ├── fake │ │ ├── doc.go │ │ ├── register.go │ │ └── clientset_generated.go │ │ ├── scheme │ │ ├── doc.go │ │ └── register.go │ │ ├── typed │ │ └── instancemgr │ │ │ └── v1alpha1 │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ ├── fake_instancemgr_client.go │ │ │ └── fake_instancegroup.go │ │ │ ├── generated_expansion.go │ │ │ ├── doc.go │ │ │ └── instancemgr_client.go │ │ └── clientset.go ├── listers │ └── instancemgr │ │ └── v1alpha1 │ │ ├── expansion_generated.go │ │ └── instancegroup.go └── informers │ └── externalversions │ ├── internalinterfaces │ └── factory_interfaces.go │ ├── instancemgr │ ├── v1alpha1 │ │ ├── interface.go │ │ └── instancegroup.go │ └── interface.go │ └── generic.go ├── Dockerfile ├── api └── instancemgr │ └── v1alpha1 │ └── groupversion_info.go ├── go.mod ├── Makefile └── README.md /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "30...90" -------------------------------------------------------------------------------- /hack/tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import _ "k8s.io/code-generator" 4 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikoproj/instance-manager/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /hack/instance-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keikoproj/instance-manager/HEAD/hack/instance-manager.png -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: instance-manager 6 | namespace: instance-manager 7 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | version: "2" 2 | domain: keikoproj.io 3 | repo: _/tmp/instancemanager 4 | resources: 5 | - group: instancemgr 6 | version: v1alpha1 7 | kind: InstanceGroup 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | coverage.html 3 | test-bdd/*.xml 4 | bin 5 | bin/* 6 | vendor 7 | vendor/* 8 | .DS_Store 9 | *.idea 10 | .windsurfrules 11 | .qodo 12 | .tool-versions 13 | -------------------------------------------------------------------------------- /test-bdd/templates/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | annotations: 5 | instancemgr.keikoproj.io/config-excluded: "true" 6 | name: instance-manager-bdd -------------------------------------------------------------------------------- /test-bdd/templates/namespace-gitops.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | annotations: 5 | instancemgr.keikoproj.io/config-excluded: "false" 6 | name: instance-manager-gitops -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: instance-manager 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: instance-manager 10 | subjects: 11 | - kind: ServiceAccount 12 | name: instance-manager 13 | namespace: instance-manager 14 | -------------------------------------------------------------------------------- /config/rbac/strategy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: instance-manager-strategy 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: instance-manager-strategy 10 | subjects: 11 | - kind: ServiceAccount 12 | name: instance-manager 13 | namespace: instance-manager 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Keiko Community Support 4 | url: https://github.com/keikoproj/community 5 | about: Please ask general questions about Keiko projects here 6 | - name: Keiko Security Issues 7 | url: https://github.com/keikoproj/instance-manager/security/policy 8 | about: Please report security vulnerabilities here 9 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | exclusions: 4 | generated: lax 5 | presets: 6 | - comments 7 | - common-false-positives 8 | - legacy 9 | - std-error-handling 10 | paths: 11 | - third_party$ 12 | - builtin$ 13 | - examples$ 14 | formatters: 15 | exclusions: 16 | generated: lax 17 | paths: 18 | - third_party$ 19 | - builtin$ 20 | - examples$ 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | - [Installation](https://github.com/keikoproj/instance-manager/blob/master/docs/INSTALL.md) 4 | - [FAQ](https://github.com/keikoproj/instance-manager/blob/master/docs/FAQ.md) 5 | 6 | ## EKS Provisioner 7 | 8 | - [API Reference](https://github.com/keikoproj/instance-manager/blob/master/docs/EKS.md#api-reference) 9 | - [Annotations](https://github.com/keikoproj/instance-manager/blob/master/docs/EKS.md#annotations) -------------------------------------------------------------------------------- /config/rbac/strategy_role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | name: instance-manager-strategy 7 | rules: 8 | - apiGroups: 9 | # Replace with API group of your choice for CRD strategy 10 | - upgrademgr.keikoproj.io 11 | resources: 12 | # Replace with API resource of your choice for CRD strategy 13 | - rollingupgrades 14 | verbs: 15 | - get 16 | - list 17 | - create 18 | - delete -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/instancemgr.keikoproj.io_instancegroups.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | #patches: 9 | 10 | # the following config is for teaching kustomize how to do kustomization for CRDs. 11 | configurations: 12 | - kustomizeconfig.yaml 13 | -------------------------------------------------------------------------------- /test-bdd/templates/instance-group-gitops.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | name: bdd-test-gitops 5 | namespace: instance-manager-gitops 6 | spec: 7 | eks: 8 | maxSize: 4 9 | minSize: 2 10 | configuration: 11 | labels: 12 | test: bdd-test-gitops 13 | instanceType: t2.small 14 | tags: 15 | - key: a-default-tag 16 | value: some-other-value 17 | - key: cr-tag 18 | value: a-tag 19 | -------------------------------------------------------------------------------- /controllers/providers/aws/eks_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/eks" 5 | "github.com/onsi/gomega" 6 | "testing" 7 | ) 8 | 9 | func TestClusterDns(t *testing.T) { 10 | var ( 11 | g = gomega.NewGomegaWithT(t) 12 | ) 13 | 14 | awsWorker := AwsWorker{} 15 | cidr := "172.16.0.0/12" 16 | ip := awsWorker.GetDNSClusterIP(&eks.Cluster{KubernetesNetworkConfig: &eks.KubernetesNetworkConfigResponse{ServiceIpv4Cidr: &cidr}}) 17 | g.Expect(ip).To(gomega.Equal("172.16.0.10")) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # PLEASE READ: 2 | 3 | # This is a comment. 4 | # Each line is a file pattern followed by one or more owners. 5 | 6 | # These owners will be the default owners for everything in 7 | # the repo. Unless a later match takes precedence, 8 | # review when someone opens a pull request. 9 | * @keikoproj/authorized-approvers @keikoproj/instance-manager-approvers 10 | 11 | # Admins own root and CI. 12 | .github/** @keikoproj/keiko-admins @keikoproj/keiko-maintainers 13 | /* @keikoproj/keiko-admins @keikoproj/keiko-maintainers 14 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ -------------------------------------------------------------------------------- /client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | // This package has the automatically generated fake clientset. 18 | package fake 19 | -------------------------------------------------------------------------------- /client/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | // This package contains the scheme of the automatically generated clientset. 18 | package scheme 19 | -------------------------------------------------------------------------------- /client/clientset/versioned/typed/instancemgr/v1alpha1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | // Package fake has the automatically generated clients. 18 | package fake 19 | -------------------------------------------------------------------------------- /client/clientset/versioned/typed/instancemgr/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | type InstanceGroupExpansion interface{} 20 | -------------------------------------------------------------------------------- /client/clientset/versioned/typed/instancemgr/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | // This package has the automatically generated typed clients. 18 | package v1alpha1 19 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | 1. Fork it () 4 | 2. Open an issue and discuss the feature / bug 5 | 3. Create your feature branch (`git checkout -b feature/fooBar`) 6 | 4. Commit your changes (`git commit -am 'Add some fooBar'`) 7 | 5. Push to the branch (`git push origin feature/fooBar`) 8 | 6. Make sure unit tests and BDD is passing 9 | 7. Create a new Pull Request 10 | 11 | ## How to report a bug 12 | 13 | * What did you do? (how to reproduce) 14 | * What did you see? (include logs and screenshots as appropriate) 15 | * What did you expect? 16 | 17 | ## How to contribute a bug fix 18 | 19 | * Open an issue and discuss it. 20 | * Create a pull request for your fix. 21 | 22 | ## How to suggest a new feature 23 | 24 | * Open an issue and discuss it. 25 | -------------------------------------------------------------------------------- /test-bdd/templates/instance-group-fargate.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | name: bdd-test-fargate 5 | namespace: instance-manager-bdd 6 | spec: 7 | provisioner: eks-fargate 8 | strategy: 9 | type: managed 10 | eks-fargate: 11 | clusterName: {{ .ClusterName }} 12 | podExecutionRoleArn: "" 13 | subnets: {{range $element := .Subnets}} 14 | - {{$element}} 15 | {{ end }} 16 | selectors: 17 | - namespace: "default" 18 | labels: 19 | key1: "value1" 20 | key2: "value2" 21 | - namespace: "instance-manager" 22 | labels: 23 | key1: "value1" 24 | key2: "value2" 25 | key3: "value3" 26 | tags: 27 | - key9: value101 28 | key7: value202 29 | key8: value9 30 | -------------------------------------------------------------------------------- /test-bdd/templates/instance-group-managed.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | name: bdd-test-managed 5 | namespace: instance-manager-bdd 6 | spec: 7 | provisioner: eks-managed 8 | strategy: 9 | type: managed 10 | eks-managed: 11 | maxSize: 4 12 | minSize: 2 13 | configuration: 14 | nodeRole: {{ .NodeRoleArn }} 15 | clusterName: {{ .ClusterName }} 16 | amiType: AL2_x86_64 17 | instanceType: t2.small 18 | subnets: {{range $element := .Subnets}} 19 | - {{$element}} 20 | {{ end }} 21 | keyPairName: {{ .KeyPairName }} 22 | securityGroups: {{range $element := .NodeSecurityGroups}} 23 | - {{$element}} 24 | {{ end }} 25 | nodeLabels: 26 | test: bdd-test-managed 27 | tags: 28 | - key: foo 29 | value: bar 30 | volSize: 20 -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: unit-test 2 | permissions: 3 | contents: read # Needed to check out the repository 4 | pull-requests: write # Needed to post test results as PR comments 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | unit-test: 14 | if: github.repository_owner == 'keikoproj' 15 | name: unit-test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Set up Go 1.x 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: 1.24 22 | 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@v4 25 | 26 | - name: Build 27 | run: | 28 | make manager 29 | 30 | - name: Test 31 | run: | 32 | make test 33 | 34 | - name: Upload coverage reports to Codecov 35 | uses: codecov/codecov-action@v5 36 | with: 37 | file: ./coverage.txt 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /client/listers/instancemgr/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by lister-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | // InstanceGroupListerExpansion allows custom methods to be added to 20 | // InstanceGroupLister. 21 | type InstanceGroupListerExpansion interface{} 22 | 23 | // InstanceGroupNamespaceListerExpansion allows custom methods to be added to 24 | // InstanceGroupNamespaceLister. 25 | type InstanceGroupNamespaceListerExpansion interface{} 26 | -------------------------------------------------------------------------------- /hack/update-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | set -o pipefail 5 | 6 | readonly REPO_ROOT="$(git rev-parse --show-toplevel)" 7 | 8 | function make_fake_paths() { 9 | FAKE_GOPATH="$(mktemp -d)" 10 | trap 'rm -rf ${FAKE_GOPATH}' EXIT 11 | FAKE_REPOPATH="${FAKE_GOPATH}/src/github.com/keikoproj/instance-manager" 12 | mkdir -p "$(dirname "${FAKE_REPOPATH}")" && ln -s "${REPO_ROOT}" "${FAKE_REPOPATH}" 13 | } 14 | 15 | make_fake_paths 16 | 17 | export GOPATH="${FAKE_GOPATH}" 18 | export GO111MODULE="off" 19 | 20 | cd "${FAKE_REPOPATH}" 21 | 22 | CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${FAKE_REPOPATH}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} 23 | 24 | echo "${FAKE_REPOPATH}" 25 | echo ${CODEGEN_PKG} 26 | 27 | chmod +x ${CODEGEN_PKG}/*.sh 28 | 29 | bash -x ${CODEGEN_PKG}/generate-groups.sh "client,informer,lister" \ 30 | github.com/keikoproj/instance-manager/client github.com/keikoproj/instance-manager/api \ 31 | "instancemgr:v1alpha1" \ 32 | --go-header-file hack/boilerplate.go.txt 33 | -------------------------------------------------------------------------------- /docs/examples/EKS-managed.md: -------------------------------------------------------------------------------- 1 | ### EKS Managed Node Group (alpha-1) 2 | 3 | You can also provision EKS managed node groups by submitting a spec with a different provisioner. 4 | 5 | ```yaml 6 | apiVersion: instancemgr.keikoproj.io/v1alpha1 7 | kind: InstanceGroup 8 | metadata: 9 | name: hello-world 10 | namespace: instance-manager 11 | spec: 12 | strategy: 13 | type: managed 14 | provisioner: eks-managed 15 | eks-managed: 16 | maxSize: 6 17 | minSize: 3 18 | configuration: 19 | clusterName: my-eks-cluster 20 | labels: 21 | example.label.com/label: some-value 22 | volSize: 20 23 | nodeRole: arn:aws:iam::012345678910:role/basic-eks-role 24 | amiType: AL2_x86_64 25 | instanceType: m5.large 26 | keyPairName: my-ec2-key-pair 27 | securityGroups: 28 | - sg-04adb6343b07c7914 29 | subnets: 30 | - subnet-0bf9bc85fd80af561 31 | - subnet-0130025d2673de5e4 32 | - subnet-01a5c28e074c46580 33 | tags: 34 | - key: my-ec2-tag 35 | value: some-value 36 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question or Support Request 3 | about: Ask a question or request support 4 | title: '[QUESTION] ' 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | ## Question 10 | 11 | 12 | 13 | ## Context 14 | 15 | 16 | 17 | 18 | ## Environment (if relevant) 19 | 20 | 21 | - Version: 22 | - Kubernetes version: 23 | - Cloud Provider: 24 | - OS: 25 | 26 | ## Screenshots/Logs (if applicable) 27 | 28 | 29 | 30 | ## Related Documentation 31 | 32 | 33 | 34 | ## Search Terms 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test-bdd/templates/instance-group.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: bdd-test-rolling 7 | namespace: instance-manager-bdd 8 | spec: 9 | provisioner: eks 10 | strategy: 11 | type: rollingUpdate 12 | rollingUpdate: 13 | maxUnavailable: 1 14 | eks: 15 | maxSize: 4 16 | minSize: 2 17 | configuration: 18 | labels: 19 | test: bdd-test-rolling 20 | taints: 21 | - key: node-role.kubernetes.io/bdd-test 22 | value: bdd-test 23 | effect: NoSchedule 24 | clusterName: {{ .ClusterName }} 25 | subnets: {{range $element := .Subnets}} 26 | - {{$element}} 27 | {{ end }} 28 | keyPairName: {{ .KeyPairName }} 29 | image: {{ .AmiID }} 30 | instanceType: t2.small 31 | volumes: 32 | - name: /dev/xvdb 33 | type: gp2 34 | size: 30 35 | mountOptions: 36 | fileSystem: xfs 37 | mount: /data 38 | securityGroups: {{range $element := .NodeSecurityGroups}} 39 | - {{$element}} 40 | {{ end }} 41 | metricsCollection: 42 | - all -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: instance-manager 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - create 13 | - get 14 | - list 15 | - patch 16 | - update 17 | - watch 18 | - apiGroups: 19 | - "" 20 | resources: 21 | - events 22 | verbs: 23 | - create 24 | - get 25 | - list 26 | - watch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - namespaces 31 | verbs: 32 | - get 33 | - list 34 | - watch 35 | - apiGroups: 36 | - "" 37 | resources: 38 | - nodes 39 | verbs: 40 | - list 41 | - patch 42 | - watch 43 | - apiGroups: 44 | - apiextensions.k8s.io 45 | resources: 46 | - customresourcedefinitions 47 | verbs: 48 | - create 49 | - delete 50 | - get 51 | - list 52 | - patch 53 | - update 54 | - watch 55 | - apiGroups: 56 | - instancemgr.keikoproj.io 57 | resources: 58 | - instancegroups 59 | verbs: 60 | - create 61 | - delete 62 | - get 63 | - list 64 | - patch 65 | - update 66 | - watch 67 | - apiGroups: 68 | - instancemgr.keikoproj.io 69 | resources: 70 | - instancegroups/status 71 | verbs: 72 | - get 73 | - patch 74 | - update 75 | -------------------------------------------------------------------------------- /test-bdd/templates/instance-group-wp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: bdd-test-rolling-wp 7 | namespace: instance-manager-bdd 8 | spec: 9 | provisioner: eks 10 | strategy: 11 | type: rollingUpdate 12 | rollingUpdate: 13 | maxUnavailable: 1 14 | eks: 15 | maxSize: 4 16 | minSize: 2 17 | warmPool: 18 | maxSize: -1 19 | minSize: 0 20 | configuration: 21 | labels: 22 | test: bdd-test-rolling-wp 23 | taints: 24 | - key: node-role.kubernetes.io/bdd-test 25 | value: bdd-test 26 | effect: NoSchedule 27 | clusterName: {{ .ClusterName }} 28 | subnets: {{range $element := .Subnets}} 29 | - {{$element}} 30 | {{ end }} 31 | keyPairName: {{ .KeyPairName }} 32 | image: {{ .AmiID }} 33 | instanceType: t2.small 34 | volumes: 35 | - name: /dev/xvdb 36 | type: gp2 37 | size: 30 38 | mountOptions: 39 | fileSystem: xfs 40 | mount: /data 41 | securityGroups: {{range $element := .NodeSecurityGroups}} 42 | - {{$element}} 43 | {{ end }} 44 | metricsCollection: 45 | - all -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: codeql 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: '16 16 * * 4' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze (${{ matrix.language }}) 14 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 15 | permissions: 16 | security-events: write 17 | packages: read 18 | actions: read 19 | contents: read 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | include: 25 | - language: actions 26 | build-mode: none 27 | - language: go 28 | build-mode: autobuild 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | - name: Set up Go 1.x 34 | uses: actions/setup-go@v5 35 | with: 36 | go-version: 1.24 37 | 38 | - name: Initialize CodeQL 39 | uses: github/codeql-action/init@v3 40 | with: 41 | languages: ${{ matrix.language }} 42 | build-mode: ${{ matrix.build-mode }} 43 | 44 | - name: Perform CodeQL Analysis 45 | uses: github/codeql-action/analyze@v3 46 | with: 47 | category: "/language:${{matrix.language}}" 48 | -------------------------------------------------------------------------------- /test-bdd/templates/instance-group-latest-locked.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | annotations: 5 | instancemgr.keikoproj.io/lock-upgrades: "false" 6 | labels: 7 | controller-tools.k8s.io: "1.0" 8 | name: bdd-test-latest-locked 9 | namespace: instance-manager-bdd 10 | spec: 11 | provisioner: eks 12 | strategy: 13 | type: rollingUpdate 14 | rollingUpdate: 15 | maxUnavailable: 1 16 | eks: 17 | maxSize: 4 18 | minSize: 2 19 | configuration: 20 | labels: 21 | test: bdd-test-latest-locked 22 | taints: 23 | - key: node-role.kubernetes.io/bdd-test 24 | value: bdd-test 25 | effect: NoSchedule 26 | clusterName: {{ .ClusterName }} 27 | subnets: {{range $element := .Subnets}} 28 | - {{$element}} 29 | {{ end }} 30 | keyPairName: {{ .KeyPairName }} 31 | image: latest 32 | instanceType: t2.small 33 | volumes: 34 | - name: /dev/xvdb 35 | type: gp2 36 | size: 30 37 | mountOptions: 38 | fileSystem: xfs 39 | mount: /data 40 | securityGroups: {{range $element := .NodeSecurityGroups}} 41 | - {{$element}} 42 | {{ end }} 43 | metricsCollection: 44 | - all -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation Issue 3 | about: Report issues with documentation or suggest improvements 4 | title: '[DOCS] ' 5 | labels: documentation 6 | assignees: '' 7 | --- 8 | 9 | ## Documentation Issue 10 | 11 | 12 | 13 | 14 | ## Page/Location 15 | 16 | 17 | 18 | 19 | ## Suggested Changes 20 | 21 | 22 | 23 | 24 | ## Additional Information 25 | 26 | 27 | 28 | 29 | ## Would you be willing to contribute this documentation improvement? 30 | 31 | 32 | - [ ] Yes, I can submit a PR with the changes 33 | - [ ] No, I'm not able to contribute documentation for this 34 | -------------------------------------------------------------------------------- /test-bdd/templates/instance-group-launch-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: bdd-test-launchtemplate 7 | namespace: instance-manager-bdd 8 | spec: 9 | provisioner: eks 10 | strategy: 11 | type: rollingUpdate 12 | rollingUpdate: 13 | maxUnavailable: 1 14 | eks: 15 | maxSize: 4 16 | minSize: 2 17 | type: LaunchTemplate 18 | warmPool: 19 | maxSize: -1 20 | minSize: 0 21 | configuration: 22 | labels: 23 | test: bdd-test-launchtemplate 24 | taints: 25 | - key: node-role.kubernetes.io/bdd-test 26 | value: bdd-test 27 | effect: NoSchedule 28 | clusterName: {{ .ClusterName }} 29 | subnets: {{range $element := .Subnets}} 30 | - {{$element}} 31 | {{ end }} 32 | keyPairName: {{ .KeyPairName }} 33 | image: {{ .AmiID }} 34 | instanceType: t2.small 35 | volumes: 36 | - name: /dev/xvdb 37 | type: gp2 38 | size: 30 39 | mountOptions: 40 | fileSystem: xfs 41 | mount: /data 42 | securityGroups: {{range $element := .NodeSecurityGroups}} 43 | - {{$element}} 44 | {{ end }} 45 | metricsCollection: 46 | - all -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | ignore: 13 | - dependency-name: "k8s.io*" ## K8s module version updates should be done explicitly 14 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 15 | - dependency-name: "sigs.k8s.io*" ## K8s module version updates should be done explicitly 16 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 17 | - dependency-name: "*" ## Major version updates should be done explicitly 18 | update-types: ["version-update:semver-major"] 19 | 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "monthly" 24 | 25 | - package-ecosystem: "docker" 26 | directory: "/" 27 | schedule: 28 | interval: "monthly" 29 | ignore: 30 | - dependency-name: "golang" ## Golang version should be done explicitly -------------------------------------------------------------------------------- /test-bdd/templates/instance-group-launch-template-mixed.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: bdd-test-launchtemplate-mixed 7 | namespace: instance-manager-bdd 8 | spec: 9 | provisioner: eks 10 | strategy: 11 | type: rollingUpdate 12 | rollingUpdate: 13 | maxUnavailable: 1 14 | eks: 15 | maxSize: 4 16 | minSize: 2 17 | type: LaunchTemplate 18 | configuration: 19 | mixedInstancesPolicy: 20 | instancePool: SubFamilyFlexible 21 | labels: 22 | test: bdd-test-launchtemplate-mixed 23 | taints: 24 | - key: node-role.kubernetes.io/bdd-test 25 | value: bdd-test 26 | effect: NoSchedule 27 | clusterName: {{ .ClusterName }} 28 | subnets: {{range $element := .Subnets}} 29 | - {{$element}} 30 | {{ end }} 31 | keyPairName: {{ .KeyPairName }} 32 | image: {{ .AmiID }} 33 | instanceType: m5.large 34 | volumes: 35 | - name: /dev/xvdb 36 | type: gp2 37 | size: 30 38 | mountOptions: 39 | fileSystem: xfs 40 | mount: /data 41 | securityGroups: {{range $element := .NodeSecurityGroups}} 42 | - {{$element}} 43 | {{ end }} 44 | metricsCollection: 45 | - all -------------------------------------------------------------------------------- /config/crd/bases/instance-manager-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: instance-manager 6 | name: instance-manager 7 | namespace: instance-manager 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: instance-manager 13 | strategy: 14 | rollingUpdate: 15 | maxSurge: 1 16 | maxUnavailable: 1 17 | type: RollingUpdate 18 | template: 19 | metadata: 20 | labels: 21 | app: instance-manager 22 | spec: 23 | containers: 24 | - image: keikoproj/instance-manager:latest 25 | imagePullPolicy: Always 26 | name: instance-manager 27 | resources: 28 | limits: 29 | cpu: 1000m 30 | memory: 1024Mi 31 | requests: 32 | cpu: 100m 33 | memory: 300Mi 34 | terminationMessagePath: /dev/termination-log 35 | terminationMessagePolicy: File 36 | volumeMounts: 37 | - name: ssl-certs 38 | mountPath: /etc/ssl/certs/ca-certificates.crt 39 | readOnly: true 40 | dnsPolicy: ClusterFirst 41 | restartPolicy: Always 42 | serviceAccount: instance-manager 43 | serviceAccountName: instance-manager 44 | volumes: 45 | - name: ssl-certs 46 | hostPath: 47 | path: /etc/ssl/certs/ca-bundle.crt -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | permissions: 3 | contents: read # Needed to check out the repository 4 | pull-requests: write # Needed to post lint results as PR comments 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: [ "master" ] 10 | 11 | env: 12 | GO_VERSION: stable 13 | GOLANGCI_LINT_VERSION: v2.1.1 14 | 15 | jobs: 16 | detect-modules: 17 | runs-on: ubuntu-latest 18 | outputs: 19 | modules: ${{ steps.set-modules.outputs.modules }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ env.GO_VERSION }} 25 | - id: set-modules 26 | run: echo "modules=$(go list -m -json | jq -s '.' | jq -c '[.[].Dir]')" >> $GITHUB_OUTPUT 27 | 28 | golangci-lint: 29 | needs: detect-modules 30 | runs-on: ubuntu-latest 31 | strategy: 32 | matrix: 33 | modules: ${{ fromJSON(needs.detect-modules.outputs.modules) }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions/setup-go@v5 37 | with: 38 | go-version: ${{ env.GO_VERSION }} 39 | - name: golangci-lint ${{ matrix.modules }} 40 | uses: golangci/golangci-lint-action@v7 41 | with: 42 | version: ${{ env.GOLANGCI_LINT_VERSION }} 43 | working-directory: ${{ matrix.modules }} 44 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We welcome participation from individuals and groups of all backgrounds who want to benefit the broader open source community 4 | through participation in this project. We are dedicated to ensuring a productive, safe and educational experience for all. 5 | 6 | ## Guidelines 7 | 8 | Be welcoming 9 | 10 | * Make it easy for new members to learn and contribute. Help them along the path. Don't make them jump through hoops. 11 | 12 | Be considerate 13 | 14 | * There is a live person at the other end of the Internet. Consider how your comments will affect them. It is often better to give a quick but useful reply than to delay to compose a more thorough reply. 15 | 16 | Be respectful 17 | 18 | * Not everyone is Linus Torvalds, and this is probably a good thing :) but everyone is deserving of respect and consideration for wanting to benefit the broader community. Criticize ideas but respect the person. Saying something positive before you criticize lets the other person know that your criticism is not personal. 19 | 20 | Be patient 21 | 22 | * We have diverse backgrounds. It will take time and effort to understand each others' points of view. Some of us have day jobs and other responsibilities and may take time to respond to requests. 23 | 24 | ## Relevant References 25 | 26 | * 27 | -------------------------------------------------------------------------------- /client/clientset/versioned/typed/instancemgr/v1alpha1/fake/fake_instancemgr_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | package fake 18 | 19 | import ( 20 | v1alpha1 "github.com/keikoproj/instance-manager/client/clientset/versioned/typed/instancemgr/v1alpha1" 21 | rest "k8s.io/client-go/rest" 22 | testing "k8s.io/client-go/testing" 23 | ) 24 | 25 | type FakeInstancemgrV1alpha1 struct { 26 | *testing.Fake 27 | } 28 | 29 | func (c *FakeInstancemgrV1alpha1) InstanceGroups(namespace string) v1alpha1.InstanceGroupInterface { 30 | return &FakeInstanceGroups{c, namespace} 31 | } 32 | 33 | // RESTClient returns a RESTClient that is used to communicate 34 | // with API server by this client implementation. 35 | func (c *FakeInstancemgrV1alpha1) RESTClient() rest.Interface { 36 | var ret *rest.RESTClient 37 | return ret 38 | } 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an enhancement or new feature 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ## Problem Statement 10 | 11 | 12 | 13 | 14 | ## Proposed Solution 15 | 16 | 17 | 18 | 19 | ## Alternatives Considered 20 | 21 | 22 | 23 | 24 | ## User Value 25 | 26 | 27 | 28 | 29 | ## Implementation Ideas 30 | 31 | 32 | 33 | 34 | ## Additional Context 35 | 36 | 37 | 38 | ## Would you be willing to contribute this feature? 39 | 40 | 41 | - [ ] Yes, I'd like to implement this feature 42 | - [ ] I could contribute partially 43 | - [ ] No, I'm not able to contribute code for this 44 | -------------------------------------------------------------------------------- /test-bdd/templates/instance-group-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: bdd-test-crd 7 | namespace: instance-manager-bdd 8 | spec: 9 | provisioner: eks 10 | strategy: 11 | type: crd 12 | crd: 13 | crdName: rollingupgrades 14 | statusJSONPath: .status.currentStatus 15 | statusSuccessString: completed 16 | statusFailureString: error 17 | spec: | 18 | apiVersion: upgrademgr.keikoproj.io/v1alpha1 19 | kind: RollingUpgrade 20 | metadata: 21 | name: rollup-nodes 22 | namespace: instance-manager 23 | spec: 24 | postDrainDelaySeconds: 10 25 | nodeIntervalSeconds: 30 26 | asgName: {{`{{ .InstanceGroup.Status.ActiveScalingGroupName }}`}} 27 | eks: 28 | maxSize: 4 29 | minSize: 2 30 | configuration: 31 | labels: 32 | test: bdd-test-crd 33 | taints: 34 | - key: node-role.kubernetes.io/bdd-test 35 | value: bdd-test 36 | effect: NoSchedule 37 | clusterName: {{ .ClusterName }} 38 | subnets: {{range $element := .Subnets}} 39 | - {{$element}} 40 | {{ end }} 41 | keyPairName: {{ .KeyPairName }} 42 | image: {{ .AmiID }} 43 | instanceType: t2.small 44 | volumes: 45 | - name: /dev/xvda 46 | type: gp2 47 | size: 30 48 | securityGroups: {{range $element := .NodeSecurityGroups}} 49 | - {{$element}} 50 | {{ end }} 51 | metricsCollection: 52 | - all -------------------------------------------------------------------------------- /client/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by informer-gen. DO NOT EDIT. 16 | 17 | package internalinterfaces 18 | 19 | import ( 20 | time "time" 21 | 22 | versioned "github.com/keikoproj/instance-manager/client/clientset/versioned" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | cache "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 29 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 30 | 31 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 32 | type SharedInformerFactory interface { 33 | Start(stopCh <-chan struct{}) 34 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 35 | } 36 | 37 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 38 | type TweakListOptionsFunc func(*v1.ListOptions) 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM --platform=$BUILDPLATFORM golang:1.24 AS builder 3 | ARG TARGETOS TARGETARCH 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | 17 | # Build 18 | RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -a -o manager main.go 19 | 20 | # Use distroless as minimal base image to package the manager binary 21 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 22 | FROM gcr.io/distroless/static:latest 23 | 24 | # Add ARG declarations to receive build args 25 | ARG CREATED 26 | ARG VERSION 27 | ARG REVISION 28 | 29 | WORKDIR / 30 | COPY --from=builder /workspace/manager . 31 | ENTRYPOINT ["/manager"] 32 | LABEL org.opencontainers.image.source="https://github.com/keikoproj/instance-manager" 33 | LABEL org.opencontainers.image.version="${VERSION}" 34 | LABEL org.opencontainers.image.created="${CREATED}" 35 | LABEL org.opencontainers.image.revision="${REVISION}" 36 | LABEL org.opencontainers.image.licenses="Apache-2.0" 37 | LABEL org.opencontainers.image.url="https://github.com/keikoproj/instance-manager/blob/master/README.md" 38 | LABEL org.opencontainers.image.description="A Kubernetes controller for creating and managing worker node instance groups across multiple providers" 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug in the project 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Bug Description 10 | 11 | 12 | 13 | ## Steps To Reproduce 14 | 15 | 16 | 17 | 1. Step one 18 | 2. Step two 19 | 3. Step three 20 | 21 | ## Expected Behavior 22 | 23 | 24 | 25 | ## Actual Behavior 26 | 27 | 28 | 29 | ## Screenshots/Logs 30 | 31 | 32 | 33 | ## Environment 34 | 35 | 36 | 37 | - Version: 38 | - Kubernetes version: 39 | - Cloud Provider: 40 | - Installation method: 41 | - OS: 42 | - Browser (if applicable): 43 | 44 | ## Additional Context 45 | 46 | 47 | 48 | 49 | ## Impact 50 | 51 | 52 | 53 | - [ ] Blocking (cannot proceed with work) 54 | - [ ] High (significant workaround needed) 55 | - [ ] Medium (minor workaround needed) 56 | - [ ] Low (inconvenience) 57 | 58 | ## Possible Solution 59 | 60 | 61 | -------------------------------------------------------------------------------- /test-bdd/templates/instance-group-crd-wp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: bdd-test-crd-wp 7 | namespace: instance-manager-bdd 8 | spec: 9 | provisioner: eks 10 | strategy: 11 | type: crd 12 | crd: 13 | crdName: rollingupgrades 14 | statusJSONPath: .status.currentStatus 15 | statusSuccessString: completed 16 | statusFailureString: error 17 | spec: | 18 | apiVersion: upgrademgr.keikoproj.io/v1alpha1 19 | kind: RollingUpgrade 20 | metadata: 21 | name: rollup-nodes 22 | namespace: instance-manager 23 | spec: 24 | postDrainDelaySeconds: 10 25 | nodeIntervalSeconds: 30 26 | asgName: {{`{{ .InstanceGroup.Status.ActiveScalingGroupName }}`}} 27 | eks: 28 | maxSize: 4 29 | minSize: 2 30 | warmPool: 31 | maxSize: -1 32 | minSize: 0 33 | configuration: 34 | labels: 35 | test: bdd-test-crd-wp 36 | taints: 37 | - key: node-role.kubernetes.io/bdd-test 38 | value: bdd-test 39 | effect: NoSchedule 40 | clusterName: {{ .ClusterName }} 41 | subnets: {{range $element := .Subnets}} 42 | - {{$element}} 43 | {{ end }} 44 | keyPairName: {{ .KeyPairName }} 45 | image: {{ .AmiID }} 46 | instanceType: t2.small 47 | volumes: 48 | - name: /dev/xvda 49 | type: gp2 50 | size: 30 51 | securityGroups: {{range $element := .NodeSecurityGroups}} 52 | - {{$element}} 53 | {{ end }} 54 | metricsCollection: 55 | - all -------------------------------------------------------------------------------- /controllers/providers/aws/retry.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/keikoproj/instance-manager/controllers/common" 8 | 9 | "github.com/aws/aws-sdk-go/aws/client" 10 | "github.com/aws/aws-sdk-go/aws/request" 11 | ) 12 | 13 | type RetryLogger struct { 14 | client.DefaultRetryer 15 | metricsCollector *common.MetricsCollector 16 | } 17 | 18 | var _ request.Retryer = &RetryLogger{} 19 | var DefaultRetryer = client.DefaultRetryer{ 20 | NumMaxRetries: 12, 21 | MinThrottleDelay: time.Second * 5, 22 | MaxThrottleDelay: time.Second * 60, 23 | MinRetryDelay: time.Second * 1, 24 | MaxRetryDelay: time.Second * 5, 25 | } 26 | 27 | func NewRetryLogger(maxRetries int, metrics *common.MetricsCollector) *RetryLogger { 28 | retryer := DefaultRetryer 29 | retryer.NumMaxRetries = maxRetries 30 | return &RetryLogger{ 31 | DefaultRetryer: retryer, 32 | metricsCollector: metrics, 33 | } 34 | } 35 | 36 | func (l RetryLogger) RetryRules(r *request.Request) time.Duration { 37 | var ( 38 | duration = l.DefaultRetryer.RetryRules(r) 39 | service = r.ClientInfo.ServiceName 40 | name string 41 | err string 42 | ) 43 | 44 | if r.Operation != nil { 45 | name = r.Operation.Name 46 | } 47 | method := fmt.Sprintf("%v/%v", service, name) 48 | 49 | if r.IsErrorThrottle() { 50 | l.metricsCollector.IncThrottle(service, name) 51 | } 52 | 53 | if r.Error != nil { 54 | err = fmt.Sprintf("%v", r.Error) 55 | } else { 56 | err = fmt.Sprintf("%d %s", r.HTTPResponse.StatusCode, r.HTTPResponse.Status) 57 | } 58 | log.V(1).Info("retryable failure", "error", err, "method", method, "backoff", duration) 59 | 60 | return duration 61 | } 62 | -------------------------------------------------------------------------------- /controllers/provisioners/eksfargate/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package eksfargate 17 | 18 | import ( 19 | "github.com/aws/aws-sdk-go/aws" 20 | "github.com/go-logr/logr" 21 | "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 22 | awsprovider "github.com/keikoproj/instance-manager/controllers/providers/aws" 23 | ) 24 | 25 | type DiscoveredState struct { 26 | ProfileStatus string 27 | } 28 | 29 | func (ds *DiscoveredState) GetProfileStatus() string { 30 | return ds.ProfileStatus 31 | } 32 | func (ds *DiscoveredState) IsProvisioned() bool { 33 | return ds.GetProfileStatus() != aws.StringValue(nil) 34 | } 35 | 36 | type FargateInstanceGroupContext struct { 37 | InstanceGroup *v1alpha1.InstanceGroup 38 | AwsWorker awsprovider.AwsWorker 39 | DiscoveredState DiscoveredState 40 | Log logr.Logger 41 | } 42 | 43 | func (ctx *FargateInstanceGroupContext) GetDiscoveredState() *DiscoveredState { 44 | return &ctx.DiscoveredState 45 | } 46 | func (ctx *FargateInstanceGroupContext) GetInstanceGroup() *v1alpha1.InstanceGroup { 47 | if ctx != nil { 48 | return ctx.InstanceGroup 49 | } 50 | return &v1alpha1.InstanceGroup{} 51 | } 52 | -------------------------------------------------------------------------------- /client/informers/externalversions/instancemgr/v1alpha1/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by informer-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | internalinterfaces "github.com/keikoproj/instance-manager/client/informers/externalversions/internalinterfaces" 21 | ) 22 | 23 | // Interface provides access to all the informers in this group version. 24 | type Interface interface { 25 | // InstanceGroups returns a InstanceGroupInformer. 26 | InstanceGroups() InstanceGroupInformer 27 | } 28 | 29 | type version struct { 30 | factory internalinterfaces.SharedInformerFactory 31 | namespace string 32 | tweakListOptions internalinterfaces.TweakListOptionsFunc 33 | } 34 | 35 | // New returns a new Interface. 36 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 37 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 38 | } 39 | 40 | // InstanceGroups returns a InstanceGroupInformer. 41 | func (v *version) InstanceGroups() InstanceGroupInformer { 42 | return &instanceGroupInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 43 | } 44 | -------------------------------------------------------------------------------- /controllers/provisioners/provisioners.go: -------------------------------------------------------------------------------- 1 | package provisioners 2 | 3 | import ( 4 | "github.com/go-logr/logr" 5 | "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 6 | corev1 "k8s.io/api/core/v1" 7 | 8 | "github.com/keikoproj/instance-manager/controllers/common" 9 | awsprovider "github.com/keikoproj/instance-manager/controllers/providers/aws" 10 | kubeprovider "github.com/keikoproj/instance-manager/controllers/providers/kubernetes" 11 | ) 12 | 13 | const ( 14 | TagClusterName = "instancegroups.keikoproj.io/ClusterName" 15 | TagInstanceGroupName = "instancegroups.keikoproj.io/InstanceGroup" 16 | TagInstanceGroupNamespace = "instancegroups.keikoproj.io/Namespace" 17 | TagClusterOwnershipFmt = "kubernetes.io/cluster/%s" 18 | TagKubernetesCluster = "KubernetesCluster" 19 | 20 | ConfigurationExclusionAnnotationKey = "instancemgr.keikoproj.io/config-excluded" 21 | UpgradeLockedAnnotationKey = "instancemgr.keikoproj.io/lock-upgrades" 22 | ) 23 | 24 | type ProvisionerInput struct { 25 | AwsWorker awsprovider.AwsWorker 26 | Kubernetes kubeprovider.KubernetesClientSet 27 | InstanceGroup *v1alpha1.InstanceGroup 28 | Configuration *corev1.ConfigMap 29 | Log logr.Logger 30 | ConfigRetention int 31 | Metrics *common.MetricsCollector 32 | DisableWinClusterInjection bool 33 | } 34 | 35 | var ( 36 | NonRetryableStates = []v1alpha1.ReconcileState{v1alpha1.ReconcileErr, v1alpha1.ReconcileReady, v1alpha1.ReconcileDeleted, v1alpha1.ReconcileLocked} 37 | ) 38 | 39 | func IsRetryable(instanceGroup *v1alpha1.InstanceGroup) bool { 40 | for _, state := range NonRetryableStates { 41 | if state == instanceGroup.GetState() { 42 | return false 43 | } 44 | } 45 | return true 46 | } 47 | -------------------------------------------------------------------------------- /client/informers/externalversions/instancemgr/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by informer-gen. DO NOT EDIT. 16 | 17 | package instancemgr 18 | 19 | import ( 20 | v1alpha1 "github.com/keikoproj/instance-manager/client/informers/externalversions/instancemgr/v1alpha1" 21 | internalinterfaces "github.com/keikoproj/instance-manager/client/informers/externalversions/internalinterfaces" 22 | ) 23 | 24 | // Interface provides access to each of this group's versions. 25 | type Interface interface { 26 | // V1alpha1 provides access to shared informers for resources in V1alpha1. 27 | V1alpha1() v1alpha1.Interface 28 | } 29 | 30 | type group struct { 31 | factory internalinterfaces.SharedInformerFactory 32 | namespace string 33 | tweakListOptions internalinterfaces.TweakListOptionsFunc 34 | } 35 | 36 | // New returns a new Interface. 37 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 38 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 39 | } 40 | 41 | // V1alpha1 returns a new v1alpha1.Interface. 42 | func (g *group) V1alpha1() v1alpha1.Interface { 43 | return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) 44 | } 45 | -------------------------------------------------------------------------------- /api/instancemgr/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | // Package v1alpha1 contains API Schema definitions for the instancemgr v1alpha1 API group 17 | // +kubebuilder:object:generate=true 18 | // +groupName=instancemgr.keikoproj.io 19 | package v1alpha1 20 | 21 | import ( 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | "sigs.k8s.io/controller-runtime/pkg/scheme" 24 | ) 25 | 26 | var ( 27 | // GroupVersion is group version used to register instance groups 28 | GroupVersion = schema.GroupVersion{Group: "instancemgr.keikoproj.io", Version: "v1alpha1"} 29 | SchemeGroupVersion = GroupVersion 30 | 31 | // GroupVersionResource is group version resource used to register instance groups 32 | GroupVersionResource = schema.GroupVersionResource{ 33 | Group: "instancemgr.keikoproj.io", 34 | Version: "v1alpha1", 35 | Resource: "instancegroups", 36 | } 37 | 38 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 39 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 40 | 41 | // AddToScheme adds the types in this group-version to the given scheme. 42 | AddToScheme = SchemeBuilder.AddToScheme 43 | ) 44 | 45 | func Resource(resource string) schema.GroupResource { 46 | return GroupVersion.WithResource(resource).GroupResource() 47 | } 48 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: instance-manager 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: instance-manager- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | #- ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 20 | #- ../webhook 21 | # [CERTMANAGER] To enable cert-manager, uncomment next line. 'WEBHOOK' components are required. 22 | #- ../certmanager 23 | 24 | #patches: 25 | #- manager_image_patch.yaml 26 | # Protect the /metrics endpoint by putting it behind auth. 27 | # Only one of manager_auth_proxy_patch.yaml and 28 | # manager_prometheus_metrics_patch.yaml should be enabled. 29 | #- manager_auth_proxy_patch.yaml 30 | # If you want your controller-manager to expose the /metrics 31 | # endpoint w/o any authn/z, uncomment the following line and 32 | # comment manager_auth_proxy_patch.yaml. 33 | # Only one of manager_auth_proxy_patch.yaml and 34 | # manager_prometheus_metrics_patch.yaml should be enabled. 35 | #- manager_prometheus_metrics_patch.yaml 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 38 | #- manager_webhook_patch.yaml 39 | 40 | # [CAINJECTION] Uncomment next line to enable the CA injection in the admission webhooks. 41 | # Uncomment 'CAINJECTION' in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 42 | # 'CERTMANAGER' needs to be enabled to use ca injection 43 | #- webhookcainjection_patch.yaml 44 | -------------------------------------------------------------------------------- /client/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | package fake 18 | 19 | import ( 20 | instancemgrv1alpha1 "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 21 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | runtime "k8s.io/apimachinery/pkg/runtime" 23 | schema "k8s.io/apimachinery/pkg/runtime/schema" 24 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 25 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 26 | ) 27 | 28 | var scheme = runtime.NewScheme() 29 | var codecs = serializer.NewCodecFactory(scheme) 30 | 31 | var localSchemeBuilder = runtime.SchemeBuilder{ 32 | instancemgrv1alpha1.AddToScheme, 33 | } 34 | 35 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 36 | // of clientsets, like in: 37 | // 38 | // import ( 39 | // "k8s.io/client-go/kubernetes" 40 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 41 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 42 | // ) 43 | // 44 | // kclientset, _ := kubernetes.NewForConfig(c) 45 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 46 | // 47 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 48 | // correctly. 49 | var AddToScheme = localSchemeBuilder.AddToScheme 50 | 51 | func init() { 52 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 53 | utilruntime.Must(AddToScheme(scheme)) 54 | } 55 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## What type of PR is this? 10 | 11 | 12 | - [ ] Bug fix (non-breaking change which fixes an issue) 13 | - [ ] Feature/Enhancement (non-breaking change which adds functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] Documentation update 16 | - [ ] Refactoring (no functional changes) 17 | - [ ] Performance improvement 18 | - [ ] Test updates 19 | - [ ] CI/CD related changes 20 | - [ ] Dependency upgrade 21 | 22 | ## Description 23 | 24 | 25 | ## Related issue(s) 26 | 27 | 28 | ## High-level overview of changes 29 | 33 | 34 | ## Testing performed 35 | 42 | 43 | ## Checklist 44 | 45 | 46 | - [ ] I've read the [CONTRIBUTING](/CONTRIBUTING.md) doc 47 | - [ ] I've added/updated tests that prove my fix is effective or that my feature works 48 | - [ ] I've added necessary documentation (if appropriate) 49 | - [ ] I've run `make test` locally and all tests pass 50 | - [ ] I've signed-off my commits with `git commit -s` for DCO verification 51 | - [ ] I've updated any relevant documentation 52 | - [ ] Code follows the style guidelines of this project 53 | 54 | ## Additional information 55 | 59 | -------------------------------------------------------------------------------- /client/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | package scheme 18 | 19 | import ( 20 | instancemgrv1alpha1 "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 21 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | runtime "k8s.io/apimachinery/pkg/runtime" 23 | schema "k8s.io/apimachinery/pkg/runtime/schema" 24 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 25 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 26 | ) 27 | 28 | var Scheme = runtime.NewScheme() 29 | var Codecs = serializer.NewCodecFactory(Scheme) 30 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 31 | var localSchemeBuilder = runtime.SchemeBuilder{ 32 | instancemgrv1alpha1.AddToScheme, 33 | } 34 | 35 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 36 | // of clientsets, like in: 37 | // 38 | // import ( 39 | // "k8s.io/client-go/kubernetes" 40 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 41 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 42 | // ) 43 | // 44 | // kclientset, _ := kubernetes.NewForConfig(c) 45 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 46 | // 47 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 48 | // correctly. 49 | var AddToScheme = localSchemeBuilder.AddToScheme 50 | 51 | func init() { 52 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 53 | utilruntime.Must(AddToScheme(Scheme)) 54 | } 55 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | **What Kubernetes versions has instance-manager being tested against?** 4 | 5 | > instance-manager has been tested with kubernetes `1.12.x` to `1.16.x` using EKS with the `eks` provisioner and the `eks-managed` provisioner for managed node groups. 6 | 7 | **What would happen if the instance-manager pod dies during reconcile?** 8 | 9 | > Like most Kubernetes controllers and CRDs, instance-manager is built to be resilient to controller failures and idempotent. In this scenario, once a new pod is Running it will continue reconciling in-flight instancegroups, and existing ones will not be affected, instance-manager is not in the data path and even if the controller is down, existing nodes will continue to run (however modifications will not be processed). 10 | 11 | **Does instance-manager "watch" resources in all namespaces?** 12 | 13 | > instancegroups are namespaced resources, a controller is usually run centrally in a cluster and will reconcile all the instancegroups for that cluster across multiple namespaces. 14 | 15 | **What happens when AWS resources are modified out-of-band of instance-manager?** 16 | 17 | > If you modify an instancegroup's autoscaling group / launch-configuration outside of instance-manager it will be changed back by the controller as soon as it notices the diff, this will most likely be when the custom resource is modified in some way, when the instance-manager controller pod is restarted, or when the default watch sync period expires. 18 | 19 | **How do Kubernetes version ugprades work with instance-manager?** 20 | 21 | > There are several upgrade strategies currently available. The most basic one is `rollingUpdate` which uses a basic form of node replacement. For example, if you patch your instancegroup and change it's `image` value, the controller will detect this drift, create a new launch configuration, and the instances will beging rotating according to the specified mechanism. Another supported strategy is `crd` which allows submitting an arbitrary custom resource for rotating the nodes - this allows implementing custom upgrade behavior if needed, or use other controllers such as `upgrade-manager`. 22 | -------------------------------------------------------------------------------- /client/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by informer-gen. DO NOT EDIT. 16 | 17 | package externalversions 18 | 19 | import ( 20 | "fmt" 21 | 22 | v1alpha1 "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 23 | schema "k8s.io/apimachinery/pkg/runtime/schema" 24 | cache "k8s.io/client-go/tools/cache" 25 | ) 26 | 27 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 28 | // sharedInformers based on type 29 | type GenericInformer interface { 30 | Informer() cache.SharedIndexInformer 31 | Lister() cache.GenericLister 32 | } 33 | 34 | type genericInformer struct { 35 | informer cache.SharedIndexInformer 36 | resource schema.GroupResource 37 | } 38 | 39 | // Informer returns the SharedIndexInformer. 40 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 41 | return f.informer 42 | } 43 | 44 | // Lister returns the GenericLister. 45 | func (f *genericInformer) Lister() cache.GenericLister { 46 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 47 | } 48 | 49 | // ForResource gives generic access to a shared informer of the matching type 50 | // TODO extend this to unknown resources with a client pool 51 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 52 | switch resource { 53 | // Group=instancemgr, Version=v1alpha1 54 | case v1alpha1.SchemeGroupVersion.WithResource("instancegroups"): 55 | return &genericInformer{resource: resource.GroupResource(), informer: f.Instancemgr().V1alpha1().InstanceGroups().Informer()}, nil 56 | 57 | } 58 | 59 | return nil, fmt.Errorf("no informer found for %v", resource) 60 | } 61 | -------------------------------------------------------------------------------- /controllers/common/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestContainsEqualFoldSubstring(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | str string 25 | substr string 26 | expected bool 27 | }{ 28 | { 29 | name: "both lowercase - match", 30 | str: "this is a test string", 31 | substr: "test", 32 | expected: true, 33 | }, 34 | { 35 | name: "str uppercase - match", 36 | str: "THIS IS A TEST STRING", 37 | substr: "test", 38 | expected: true, 39 | }, 40 | { 41 | name: "substr uppercase - match", 42 | str: "this is a test string", 43 | substr: "TEST", 44 | expected: true, 45 | }, 46 | { 47 | name: "mixed case - match", 48 | str: "This IS a TEST String", 49 | substr: "TeSt", 50 | expected: true, 51 | }, 52 | { 53 | name: "not found", 54 | str: "this is a test string", 55 | substr: "banana", 56 | expected: false, 57 | }, 58 | { 59 | name: "empty string", 60 | str: "", 61 | substr: "test", 62 | expected: false, 63 | }, 64 | { 65 | name: "empty substring", 66 | str: "this is a test string", 67 | substr: "", 68 | expected: true, // Empty substring is always found in any string - strings.Contains behavior 69 | }, 70 | } 71 | 72 | for _, tc := range tests { 73 | t.Run(tc.name, func(t *testing.T) { 74 | result := ContainsEqualFoldSubstring(tc.str, tc.substr) 75 | if result != tc.expected { 76 | t.Errorf("ContainsEqualFoldSubstring(%q, %q) = %v, expected %v", tc.str, tc.substr, result, tc.expected) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /controllers/provisioners/eks/state.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package eks 17 | 18 | import ( 19 | "github.com/aws/aws-sdk-go/aws" 20 | "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 21 | ) 22 | 23 | const ( 24 | ScalingGroupDeletionStatus = "Delete in progress" 25 | ) 26 | 27 | func (ctx *EksInstanceGroupContext) StateDiscovery() { 28 | var ( 29 | instanceGroup = ctx.GetInstanceGroup() 30 | state = ctx.GetDiscoveredState() 31 | provisioned = state.IsProvisioned() 32 | group = state.GetScalingGroup() 33 | ) 34 | // only discover state if it's a new reconcile 35 | if instanceGroup.GetState() != v1alpha1.ReconcileInit { 36 | return 37 | } 38 | 39 | var deleted bool 40 | if !ctx.InstanceGroup.DeletionTimestamp.IsZero() { 41 | deleted = true 42 | } 43 | 44 | if deleted { 45 | // resource is being deleted 46 | if provisioned { 47 | // scaling group still provisioned 48 | if aws.StringValue(group.Status) == ScalingGroupDeletionStatus { 49 | // scaling group is being deleted 50 | ctx.SetState(v1alpha1.ReconcileDeleting) 51 | } else { 52 | // scaling group still exists 53 | ctx.SetState(v1alpha1.ReconcileInitDelete) 54 | } 55 | } else { 56 | // scaling group does not exist 57 | ctx.SetState(v1alpha1.ReconcileDeleted) 58 | } 59 | } else { 60 | // resource is not being deleted 61 | if provisioned { 62 | // scaling group exists 63 | ctx.SetState(v1alpha1.ReconcileInitUpdate) 64 | } else { 65 | // scaling group does not exist 66 | ctx.SetState(v1alpha1.ReconcileInitCreate) 67 | } 68 | } 69 | 70 | } 71 | 72 | func (ctx *EksInstanceGroupContext) IsReady() bool { 73 | instanceGroup := ctx.GetInstanceGroup() 74 | return instanceGroup.GetState() == v1alpha1.ReconcileModified 75 | } 76 | -------------------------------------------------------------------------------- /test-bdd/templates/manager-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | boundaries: | 4 | shared: 5 | mergeOverride: 6 | - spec.eks.configuration.tags 7 | merge: 8 | - spec.strategy 9 | - spec.eks.configuration.securityGroups 10 | - spec.eks.configuration.labels 11 | - spec.eks.configuration.volumes 12 | restricted: 13 | - spec.provisioner 14 | - spec.eks.configuration.clusterName 15 | - spec.eks.configuration.image 16 | - spec.eks.configuration.roleName 17 | - spec.eks.configuration.instanceProfileName 18 | - spec.eks.configuration.keyPairName 19 | - spec.eks.configuration.subnets 20 | - spec.eks.configuration.taints 21 | defaults: | 22 | spec: 23 | provisioner: eks 24 | strategy: 25 | type: crd 26 | crd: 27 | crdName: rollingupgrades 28 | statusJSONPath: .status.currentStatus 29 | statusSuccessString: completed 30 | statusFailureString: error 31 | spec: | 32 | apiVersion: upgrademgr.keikoproj.io/v1alpha1 33 | kind: RollingUpgrade 34 | metadata: 35 | name: rollup-nodes 36 | namespace: instance-manager 37 | spec: 38 | postDrainDelaySeconds: 30 39 | nodeIntervalSeconds: 180 40 | asgName: {{`{{ .InstanceGroup.Status.ActiveScalingGroupName }}`}} 41 | eks: 42 | configuration: 43 | clusterName: {{ .ClusterName }} 44 | subnets: {{range $element := .Subnets}} 45 | - {{$element}} 46 | {{ end }} 47 | keyPairName: {{ .KeyPairName }} 48 | image: {{ .AmiID }} 49 | labels: 50 | a-default-label: "true" 51 | taints: 52 | - key: default-taint 53 | value: a-taint 54 | effect: NoSchedule 55 | volumes: 56 | - size: 30 57 | type: gp2 58 | name: /dev/xvda 59 | roleName: {{ .NodeRole }} 60 | instanceProfileName: {{ .NodeRole }} 61 | tags: 62 | - key: a-default-tag 63 | value: some-default-value 64 | securityGroups: {{range $element := .NodeSecurityGroups}} 65 | - {{$element}} 66 | {{ end }} 67 | 68 | kind: ConfigMap 69 | metadata: 70 | name: instance-manager 71 | namespace: instance-manager -------------------------------------------------------------------------------- /.github/workflows/functional-test.yaml: -------------------------------------------------------------------------------- 1 | name: functional-test 2 | permissions: 3 | contents: read # Needed to check out the repository 4 | pull-requests: write # Needed if test results are posted as PR comments 5 | 6 | on: 7 | workflow_dispatch: 8 | schedule: 9 | # run at 23:05 PM PST (cron uses UTC) 10 | - cron: '5 15 * * *' 11 | 12 | jobs: 13 | 14 | functional-test: 15 | name: functional-test 16 | if: false # Disable the workflow since AWS test environment has been removed 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | 21 | - name: python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: 'pypy3.9' 25 | architecture: 'x64' 26 | 27 | - name: setup 28 | run: | 29 | sudo apt update 30 | sudo apt install python3-pip 31 | pip install --user awscli 32 | curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl 33 | chmod +x ./kubectl 34 | sudo mv ./kubectl $HOME/.local/bin 35 | curl -sSL -o $HOME/.local/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 && chmod +x $HOME/.local/bin/jq 36 | export PATH=$PATH:$HOME/.local/bin 37 | jq --version 38 | kubectl 39 | aws --version 40 | 41 | - name: Check out code into the Go module directory 42 | uses: actions/checkout@v4 43 | with: 44 | ref: master 45 | 46 | - name: test 47 | env: 48 | AWS_REGION: us-west-2 49 | AWS_DEFAULT_REGION: us-west-2 50 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 51 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 52 | NODE_SUBNETS: ${{ secrets.NODE_SUBNETS }} 53 | NODE_ROLE: ${{ secrets.NODE_ROLE }} 54 | NODE_ROLE_ARN: ${{ secrets.NODE_ROLE_ARN }} 55 | KEYPAIR_NAME: ${{ secrets.KEYPAIR_NAME }} 56 | AMI_ID: ${{ secrets.AMI_ID }} 57 | SECURITY_GROUPS: ${{ secrets.SECURITY_GROUPS }} 58 | EKS_CLUSTER: ${{ secrets.CLUSTER_NAME }} 59 | run: | 60 | HEAD=$(git rev-parse --short HEAD) 61 | aws eks update-kubeconfig --name $EKS_CLUSTER 62 | make install 63 | kubectl set image -n instance-manager deployment/instance-manager instance-manager=keikoproj/instance-manager:master 64 | make bdd 65 | -------------------------------------------------------------------------------- /controllers/providers/kubernetes/spot.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package kubernetes 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "sort" 23 | "time" 24 | 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/client-go/kubernetes" 27 | ) 28 | 29 | const ( 30 | SpotRecommendationReason = "SpotRecommendationGiven" 31 | SpotRecommendationVersion = "v1alpha1" 32 | ) 33 | 34 | type SpotRecommendation struct { 35 | APIVersion string `yaml:"apiVersion"` 36 | SpotPrice string `yaml:"spotPrice"` 37 | UseSpot bool `yaml:"useSpot"` 38 | EventTime time.Time 39 | } 40 | 41 | type SpotReccomendationList []SpotRecommendation 42 | 43 | func GetSpotRecommendation(kube kubernetes.Interface, identifier string) (SpotRecommendation, error) { 44 | var recommendations SpotReccomendationList 45 | 46 | fieldSelector := fmt.Sprintf("reason=%v,involvedObject.name=%v", SpotRecommendationReason, identifier) 47 | 48 | eventList, err := kube.CoreV1().Events("").List(context.Background(), metav1.ListOptions{ 49 | FieldSelector: fieldSelector, 50 | }) 51 | if err != nil { 52 | return SpotRecommendation{}, err 53 | } 54 | 55 | recommendation := &SpotRecommendation{} 56 | for _, event := range eventList.Items { 57 | err := json.Unmarshal([]byte(event.Message), recommendation) 58 | if err != nil { 59 | return SpotRecommendation{}, err 60 | } 61 | recommendation.EventTime = event.LastTimestamp.Time 62 | recommendations = append(recommendations, *recommendation) 63 | } 64 | sort.Sort(sort.Reverse(recommendations)) 65 | 66 | if len(recommendations) == 0 { 67 | return SpotRecommendation{}, nil 68 | } 69 | 70 | return recommendations[0], nil 71 | } 72 | 73 | func (p SpotReccomendationList) Len() int { 74 | return len(p) 75 | } 76 | 77 | func (p SpotReccomendationList) Less(i, j int) bool { 78 | return p[i].EventTime.Before(p[j].EventTime) 79 | } 80 | 81 | func (p SpotReccomendationList) Swap(i, j int) { 82 | p[i], p[j] = p[j], p[i] 83 | } 84 | -------------------------------------------------------------------------------- /controllers/provisioners/eks/scaling/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package scaling 17 | 18 | import ( 19 | "github.com/aws/aws-sdk-go/service/autoscaling" 20 | "github.com/aws/aws-sdk-go/service/ec2" 21 | "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | ) 24 | 25 | var ( 26 | log = ctrl.Log.WithName("scaling") 27 | ) 28 | 29 | type Configuration interface { 30 | Name() string 31 | Resource() interface{} 32 | Create(input *CreateConfigurationInput) error 33 | Delete(input *DeleteConfigurationInput) error 34 | Discover(input *DiscoverConfigurationInput) error 35 | Drifted(input *CreateConfigurationInput) bool 36 | RotationNeeded(input *DiscoverConfigurationInput) bool 37 | Provisioned() bool 38 | } 39 | 40 | type DeleteConfigurationInput struct { 41 | Name string 42 | Prefix string 43 | DeleteAll bool 44 | RetainVersions int 45 | } 46 | 47 | type DiscoverConfigurationInput struct { 48 | ScalingGroup *autoscaling.Group 49 | TargetConfigName string 50 | } 51 | 52 | type CreateConfigurationInput struct { 53 | Name string 54 | IamInstanceProfileArn string 55 | ImageId string 56 | InstanceType string 57 | KeyName string 58 | SecurityGroups []string 59 | Volumes []v1alpha1.NodeVolume 60 | UserData string 61 | SpotPrice string 62 | LicenseSpecifications []string 63 | Placement *v1alpha1.PlacementSpec 64 | MetadataOptions *v1alpha1.MetadataOptions 65 | } 66 | 67 | func ConvertToLaunchTemplate(resource interface{}) *ec2.LaunchTemplate { 68 | if lt, ok := resource.(*ec2.LaunchTemplate); ok && lt != nil { 69 | return lt 70 | } 71 | return &ec2.LaunchTemplate{} 72 | } 73 | 74 | func ConvertToLaunchConfiguration(resource interface{}) *autoscaling.LaunchConfiguration { 75 | if lc, ok := resource.(*autoscaling.LaunchConfiguration); ok && lc != nil { 76 | return lc 77 | } 78 | return &autoscaling.LaunchConfiguration{} 79 | } 80 | -------------------------------------------------------------------------------- /test-bdd/features/04_delete.feature: -------------------------------------------------------------------------------- 1 | Feature: CRUD Delete 2 | In order to delete instance-groups 3 | As an EKS cluster operator 4 | I need to delete the custom resource 5 | 6 | Scenario: Resources can be deleted 7 | Given an EKS cluster 8 | Then I delete a resource instance-group.yaml 9 | And I delete a resource instance-group-crd.yaml 10 | And I delete a resource instance-group-wp.yaml 11 | And I delete a resource instance-group-crd-wp.yaml 12 | And I delete a resource instance-group-launch-template.yaml 13 | And I delete a resource instance-group-launch-template-mixed.yaml 14 | And I delete a resource instance-group-managed.yaml 15 | And I delete a resource instance-group-fargate.yaml 16 | And I delete a resource instance-group-gitops.yaml 17 | And I delete a resource instance-group-latest-locked.yaml 18 | 19 | Scenario: Delete an instance-group with rollingUpdate strategy 20 | Given an EKS cluster 21 | When I delete a resource instance-group.yaml 22 | Then 0 nodes should be found 23 | And the resource should be deleted 24 | 25 | Scenario: Delete an instance-group with CRD strategy 26 | Given an EKS cluster 27 | When I delete a resource instance-group-crd.yaml 28 | Then 0 nodes should be found 29 | And the resource should be deleted 30 | 31 | Scenario: Delete an instance-group with launch template 32 | Given an EKS cluster 33 | When I delete a resource instance-group-launch-template.yaml 34 | Then 0 nodes should be found 35 | And the resource should be deleted 36 | 37 | Scenario: Delete an instance-group with launch template and mixed instances 38 | Given an EKS cluster 39 | When I delete a resource instance-group-launch-template-mixed.yaml 40 | Then 0 nodes should be found 41 | And the resource should be deleted 42 | 43 | Scenario: Delete an instance-group with managed node-group 44 | Given an EKS cluster 45 | When I delete a resource instance-group-managed.yaml 46 | Then 0 nodes should be found 47 | And the resource should be deleted 48 | 49 | Scenario: Delete a fargate profile 50 | Given an EKS cluster 51 | Then I delete a resource instance-group-fargate.yaml 52 | And the resource should be deleted 53 | 54 | Scenario: Delete a locked profile 55 | Given an EKS cluster 56 | When I delete a resource instance-group-latest-locked.yaml 57 | Then 0 nodes should be found 58 | And the resource should be deleted 59 | 60 | Scenario: Delete an instance-group with shortened resource 61 | Given an EKS cluster 62 | When I delete a resource instance-group-gitops.yaml 63 | Then 0 nodes should be found 64 | And the resource should be deleted 65 | And I delete a resource manager-configmap.yaml -------------------------------------------------------------------------------- /controllers/common/bootstrap.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package common 17 | 18 | import ( 19 | "strings" 20 | "time" 21 | 22 | awsauth "github.com/keikoproj/aws-auth/pkg/mapper" 23 | "k8s.io/client-go/kubernetes" 24 | ) 25 | 26 | func GetGroupsForOsFamily(osFamily string) []string { 27 | if strings.EqualFold(osFamily, "windows") { 28 | return []string{ 29 | "system:bootstrappers", 30 | "system:nodes", 31 | "eks:kube-proxy-windows", 32 | } 33 | } else { 34 | return []string{ 35 | "system:bootstrappers", 36 | "system:nodes", 37 | } 38 | } 39 | } 40 | 41 | func GetNodeBootstrapUpsert(arn string, osFamily string) *awsauth.MapperArguments { 42 | return &awsauth.MapperArguments{ 43 | MapRoles: true, 44 | RoleARN: arn, 45 | Username: "system:node:{{EC2PrivateDNSName}}", 46 | Groups: GetGroupsForOsFamily(osFamily), 47 | WithRetries: true, 48 | MinRetryTime: time.Millisecond * 100, 49 | MaxRetryTime: time.Second * 30, 50 | MaxRetryCount: 12, 51 | } 52 | } 53 | 54 | func GetNodeBootstrapRemove(arn string, osFamily string) *awsauth.MapperArguments { 55 | return &awsauth.MapperArguments{ 56 | MapRoles: true, 57 | RoleARN: arn, 58 | Force: true, 59 | Username: "system:node:{{EC2PrivateDNSName}}", 60 | Groups: GetGroupsForOsFamily(osFamily), 61 | WithRetries: true, 62 | MinRetryTime: time.Millisecond * 100, 63 | MaxRetryTime: time.Second * 30, 64 | MaxRetryCount: 12, 65 | } 66 | } 67 | 68 | func RemoveAuthConfigMap(kube kubernetes.Interface, arns []string, osFamilies []string) error { 69 | authMap := awsauth.New(kube, false) 70 | for index, arn := range arns { 71 | if arn == "" { 72 | continue 73 | } 74 | err := authMap.Remove(GetNodeBootstrapRemove(arn, osFamilies[index])) 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | func UpsertAuthConfigMap(kube kubernetes.Interface, arns []string, osFamilies []string) error { 83 | authMap := awsauth.New(kube, false) 84 | for index, arn := range arns { 85 | if arn == "" { 86 | continue 87 | } 88 | err := authMap.Upsert(GetNodeBootstrapUpsert(arn, osFamilies[index])) 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /controllers/interface.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | v1alpha "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 5 | ) 6 | 7 | // CloudDeployer is a common interface that should be fulfilled by each provisioner 8 | type CloudDeployer interface { 9 | CloudDiscovery() error // Discover cloud resources 10 | StateDiscovery() // Derive state 11 | Create() error // CREATE Operation 12 | Update() error // UPDATE Operation 13 | Delete() error // DELETE Operation 14 | UpgradeNodes() error // Process upgrade strategy 15 | BootstrapNodes() error // Bootstrap Provisioned Resources 16 | GetState() v1alpha.ReconcileState // Gets the current state type of the instance group 17 | SetState(v1alpha.ReconcileState) // Sets the current state of the instance group 18 | IsReady() bool // Returns true if state is Ready 19 | Locked() bool // Returns true if instanceGroup is locked 20 | } 21 | 22 | func HandleReconcileRequest(d CloudDeployer) error { 23 | // Cloud Discovery 24 | err := d.CloudDiscovery() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | // State Discovery 30 | d.StateDiscovery() 31 | 32 | // CRUD Delete 33 | if d.GetState() == v1alpha.ReconcileInitDelete { 34 | err = d.Delete() 35 | if err != nil { 36 | return err 37 | } 38 | } 39 | 40 | // CRUD Create 41 | if d.GetState() == v1alpha.ReconcileInitCreate { 42 | err = d.Create() 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | 48 | // CRUD Update 49 | if d.GetState() == v1alpha.ReconcileInitUpdate { 50 | err = d.Update() 51 | if err != nil { 52 | return err 53 | } 54 | } 55 | 56 | // CRUD Nodes Upgrade Strategy 57 | if d.GetState() == v1alpha.ReconcileInitUpgrade { 58 | // Locked 59 | if d.Locked() { 60 | d.SetState(v1alpha.ReconcileLocked) 61 | return nil 62 | } 63 | err = d.UpgradeNodes() 64 | if err != nil { 65 | return err 66 | } 67 | } 68 | 69 | // CRUD Error 70 | if d.GetState() == v1alpha.ReconcileErr { 71 | return err 72 | } 73 | 74 | // Bootstrap Nodes 75 | if d.IsReady() { 76 | 77 | err = d.BootstrapNodes() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | if d.GetState() == v1alpha.ReconcileInitUpgrade { 83 | // Locked 84 | if d.Locked() { 85 | d.SetState(v1alpha.ReconcileLocked) 86 | return nil 87 | } 88 | err = d.UpgradeNodes() 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | 94 | // Set Ready state (external end state) 95 | if d.GetState() == v1alpha.ReconcileModified { 96 | d.SetState(v1alpha.ReconcileReady) 97 | } 98 | } 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /controllers/provisioners/eksmanaged/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package eksmanaged 17 | 18 | import ( 19 | "github.com/aws/aws-sdk-go/service/eks" 20 | "github.com/go-logr/logr" 21 | "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 22 | "github.com/keikoproj/instance-manager/controllers/providers/aws" 23 | kubeprovider "github.com/keikoproj/instance-manager/controllers/providers/kubernetes" 24 | ) 25 | 26 | type EksManagedDefaultConfiguration struct { 27 | DefaultSubnets []string `yaml:"defaultSubnets,omitempty"` 28 | EksClusterName string `yaml:"defaultClusterName,omitempty"` 29 | } 30 | 31 | type EksManagedInstanceGroupContext struct { 32 | InstanceGroup *v1alpha1.InstanceGroup 33 | KubernetesClient kubeprovider.KubernetesClientSet 34 | AwsWorker aws.AwsWorker 35 | DiscoveredState *DiscoveredState 36 | Log logr.Logger 37 | } 38 | type DiscoveredState struct { 39 | Provisioned bool 40 | SelfNodeGroup *eks.Nodegroup 41 | CurrentState string 42 | } 43 | 44 | func (d *DiscoveredState) SetSelfNodeGroup(ng *eks.Nodegroup) { 45 | d.SelfNodeGroup = ng 46 | } 47 | 48 | func (d *DiscoveredState) GetSelfNodeGroup() *eks.Nodegroup { 49 | return d.SelfNodeGroup 50 | } 51 | 52 | func (d *DiscoveredState) SetProvisioned(provisioned bool) { 53 | d.Provisioned = provisioned 54 | } 55 | 56 | func (d *DiscoveredState) SetCurrentState(state string) { 57 | d.CurrentState = state 58 | } 59 | 60 | func (d *DiscoveredState) GetCurrentState() string { 61 | return d.CurrentState 62 | } 63 | 64 | func (d *DiscoveredState) IsProvisioned() bool { 65 | return d.Provisioned 66 | } 67 | 68 | func (ctx *EksManagedInstanceGroupContext) GetInstanceGroup() *v1alpha1.InstanceGroup { 69 | if ctx != nil { 70 | return ctx.InstanceGroup 71 | } 72 | return &v1alpha1.InstanceGroup{} 73 | } 74 | 75 | func (ctx *EksManagedInstanceGroupContext) GetState() v1alpha1.ReconcileState { 76 | return ctx.InstanceGroup.GetState() 77 | } 78 | 79 | func (ctx *EksManagedInstanceGroupContext) SetState(state v1alpha1.ReconcileState) { 80 | ctx.InstanceGroup.SetState(state) 81 | } 82 | 83 | func (ctx *EksManagedInstanceGroupContext) GetDiscoveredState() *DiscoveredState { 84 | if ctx != nil { 85 | return ctx.DiscoveredState 86 | } 87 | return &DiscoveredState{} 88 | } 89 | 90 | func (ctx *EksManagedInstanceGroupContext) SetDiscoveredState(state *DiscoveredState) { 91 | ctx.DiscoveredState = state 92 | } 93 | -------------------------------------------------------------------------------- /controllers/providers/kubernetes/rollingupdate.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package kubernetes 17 | 18 | import ( 19 | awsprovider "github.com/keikoproj/instance-manager/controllers/providers/aws" 20 | corev1 "k8s.io/api/core/v1" 21 | ctrl "sigs.k8s.io/controller-runtime" 22 | ) 23 | 24 | const ( 25 | RollingUpdateStrategyName = "rollingupdate" 26 | ) 27 | 28 | var ( 29 | log = ctrl.Log.WithName("kubernetes-provider") 30 | ) 31 | 32 | type RollingUpdateRequest struct { 33 | AwsWorker awsprovider.AwsWorker 34 | ClusterNodes *corev1.NodeList 35 | ScalingGroupName string 36 | MaxUnavailable int 37 | DesiredCapacity int 38 | AllInstances []string 39 | UpdateTargets []string 40 | } 41 | 42 | func ProcessRollingUpgradeStrategy(req *RollingUpdateRequest) (bool, error) { 43 | 44 | log.Info("starting rolling update", 45 | "scalinggroup", req.ScalingGroupName, 46 | "targets", req.UpdateTargets, 47 | "maxunavailable", req.MaxUnavailable, 48 | ) 49 | if len(req.UpdateTargets) == 0 { 50 | log.Info("no updatable instances", "scalinggroup", req.ScalingGroupName) 51 | return true, nil 52 | } 53 | 54 | // cannot rotate if maxUnavailable is greater than number of desired 55 | if req.MaxUnavailable > req.DesiredCapacity { 56 | log.Info("maxUnavailable exceeds desired capacity, setting maxUnavailable match desired", 57 | "scalinggroup", req.ScalingGroupName, 58 | "maxunavailable", req.MaxUnavailable, 59 | "desiredcapacity", req.DesiredCapacity, 60 | ) 61 | req.MaxUnavailable = req.DesiredCapacity 62 | } 63 | 64 | ok, err := IsMinNodesReady(req.ClusterNodes, req.AllInstances, req.MaxUnavailable) 65 | if err != nil { 66 | return false, err 67 | } 68 | 69 | if !ok { 70 | log.Info("desired nodes are not ready", "scalinggroup", req.ScalingGroupName) 71 | return false, nil 72 | } 73 | 74 | var terminateTargets []string 75 | if req.MaxUnavailable <= len(req.UpdateTargets) { 76 | terminateTargets = req.UpdateTargets[:req.MaxUnavailable] 77 | } else { 78 | terminateTargets = req.UpdateTargets 79 | } 80 | 81 | log.Info("terminating targets", "scalinggroup", req.ScalingGroupName, "targets", terminateTargets) 82 | if err := req.AwsWorker.TerminateScalingInstances(terminateTargets); err != nil { 83 | // terminate failures are retryable 84 | log.Info("failed to terminate targets", "reason", err.Error(), "scalinggroup", req.ScalingGroupName, "targets", terminateTargets) 85 | return false, nil 86 | } 87 | return false, nil 88 | } 89 | -------------------------------------------------------------------------------- /client/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | package fake 18 | 19 | import ( 20 | clientset "github.com/keikoproj/instance-manager/client/clientset/versioned" 21 | instancemgrv1alpha1 "github.com/keikoproj/instance-manager/client/clientset/versioned/typed/instancemgr/v1alpha1" 22 | fakeinstancemgrv1alpha1 "github.com/keikoproj/instance-manager/client/clientset/versioned/typed/instancemgr/v1alpha1/fake" 23 | "k8s.io/apimachinery/pkg/runtime" 24 | "k8s.io/apimachinery/pkg/watch" 25 | "k8s.io/client-go/discovery" 26 | fakediscovery "k8s.io/client-go/discovery/fake" 27 | "k8s.io/client-go/testing" 28 | ) 29 | 30 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 31 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 32 | // without applying any validations and/or defaults. It shouldn't be considered a replacement 33 | // for a real clientset and is mostly useful in simple unit tests. 34 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 35 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 36 | for _, obj := range objects { 37 | if err := o.Add(obj); err != nil { 38 | panic(err) 39 | } 40 | } 41 | 42 | cs := &Clientset{tracker: o} 43 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 44 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 45 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 46 | gvr := action.GetResource() 47 | ns := action.GetNamespace() 48 | watch, err := o.Watch(gvr, ns) 49 | if err != nil { 50 | return false, nil, err 51 | } 52 | return true, watch, nil 53 | }) 54 | 55 | return cs 56 | } 57 | 58 | // Clientset implements clientset.Interface. Meant to be embedded into a 59 | // struct to get a default implementation. This makes faking out just the method 60 | // you want to test easier. 61 | type Clientset struct { 62 | testing.Fake 63 | discovery *fakediscovery.FakeDiscovery 64 | tracker testing.ObjectTracker 65 | } 66 | 67 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 68 | return c.discovery 69 | } 70 | 71 | func (c *Clientset) Tracker() testing.ObjectTracker { 72 | return c.tracker 73 | } 74 | 75 | var ( 76 | _ clientset.Interface = &Clientset{} 77 | _ testing.FakeClient = &Clientset{} 78 | ) 79 | 80 | // InstancemgrV1alpha1 retrieves the InstancemgrV1alpha1Client 81 | func (c *Clientset) InstancemgrV1alpha1() instancemgrv1alpha1.InstancemgrV1alpha1Interface { 82 | return &fakeinstancemgrv1alpha1.FakeInstancemgrV1alpha1{Fake: &c.Fake} 83 | } 84 | -------------------------------------------------------------------------------- /controllers/providers/aws/ssm.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/request" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/ssm" 10 | "github.com/aws/aws-sdk-go/service/ssm/ssmiface" 11 | "github.com/keikoproj/aws-sdk-go-cache/cache" 12 | "github.com/keikoproj/instance-manager/controllers/common" 13 | ) 14 | 15 | type architectureMap map[string]string 16 | 17 | const ( 18 | EksOptimisedAmiPath = "/aws/service/eks/optimized-ami/%s/amazon-linux-2/%s/image_id" 19 | EksOptimisedAmazonLinux2Arm64 = "/aws/service/eks/optimized-ami/%s/amazon-linux-2-arm64/%s/image_id" 20 | EksOptimisedBottlerocket = "/aws/service/bottlerocket/aws-k8s-%s/x86_64/%s/image_id" 21 | EksOptimisedBottlerocketArm64 = "/aws/service/bottlerocket/aws-k8s-%s/arm64/%s/image_id" 22 | EksOptimisedWindowsCore = "/aws/service/ami-windows-%s/Windows_Server-2019-English-Core-EKS_Optimized-%s/image_id" 23 | EksOptimisedWindowsFull = "/aws/service/ami-windows-%s/Windows_Server-2019-English-Full-EKS_Optimized-%s/image_id" 24 | ) 25 | 26 | var ( 27 | EksAmis = map[string]architectureMap{ 28 | "amazonlinux2": architectureMap{ 29 | "x86_64": EksOptimisedAmiPath, 30 | "arm64": EksOptimisedAmazonLinux2Arm64, 31 | }, 32 | "bottlerocket": architectureMap{ 33 | "x86_64": EksOptimisedBottlerocket, 34 | "arm64": EksOptimisedBottlerocketArm64, 35 | }, 36 | "windows": architectureMap{ 37 | "x86_64": EksOptimisedWindowsCore, 38 | }, 39 | } 40 | LatestIdentifiers = map[string]string{ 41 | "bottlerocket": "latest", 42 | "amazonlinux2": "recommended", 43 | "windows": "latest", 44 | } 45 | ) 46 | 47 | func GetAwsSsmClient(region string, cacheCfg *cache.Config, maxRetries int, collector *common.MetricsCollector) ssmiface.SSMAPI { 48 | config := aws.NewConfig().WithRegion(region).WithCredentialsChainVerboseErrors(true) 49 | config = request.WithRetryer(config, NewRetryLogger(maxRetries, collector)) 50 | sess, err := session.NewSession(config) 51 | if err != nil { 52 | panic(err) 53 | } 54 | cache.AddCaching(sess, cacheCfg) 55 | cacheCfg.SetCacheTTL("ssm", "GetParameter", GetParameterTTL) 56 | sess.Handlers.Complete.PushFront(func(r *request.Request) { 57 | ctx := r.HTTPRequest.Context() 58 | log.V(1).Info("AWS API call", 59 | "cacheHit", cache.IsCacheHit(ctx), 60 | "service", r.ClientInfo.ServiceName, 61 | "operation", r.Operation.Name, 62 | ) 63 | }) 64 | return ssm.New(sess) 65 | } 66 | 67 | func (w *AwsWorker) GetEksLatestAmi(OSFamily string, arch string, kubernetesVersion string) (string, error) { 68 | return w.GetEksSsmAmi(OSFamily, arch, kubernetesVersion, LatestIdentifiers[OSFamily]) 69 | } 70 | 71 | func (w *AwsWorker) GetEksSsmAmi(OSFamily string, arch string, kubernetesVersion string, ssmId string) (string, error) { 72 | var inputString = aws.String(fmt.Sprintf(EksAmis[OSFamily][arch], kubernetesVersion, ssmId)) 73 | if OSFamily == "windows" { 74 | inputString = aws.String(fmt.Sprintf(EksAmis[OSFamily][arch], ssmId, kubernetesVersion)) 75 | } 76 | input := &ssm.GetParameterInput{ 77 | Name: inputString, 78 | } 79 | 80 | output, err := w.SsmClient.GetParameter(input) 81 | if err != nil { 82 | return "", err 83 | } 84 | return aws.StringValue(output.Parameter.Value), nil 85 | } 86 | -------------------------------------------------------------------------------- /controllers/common/metrics.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | const ( 8 | namespace = "instance_manager" 9 | ) 10 | 11 | type MetricsCollector struct { 12 | prometheus.Collector 13 | 14 | successCounter *prometheus.CounterVec 15 | failureCounter *prometheus.CounterVec 16 | throttleCounter *prometheus.CounterVec 17 | statusGauge *prometheus.GaugeVec 18 | } 19 | 20 | func NewMetricsCollector() *MetricsCollector { 21 | return &MetricsCollector{ 22 | successCounter: prometheus.NewCounterVec( 23 | prometheus.CounterOpts{ 24 | Namespace: namespace, 25 | Name: "reconcile_success_total", 26 | Help: `total successful reconciles`, 27 | }, 28 | []string{"instancegroup"}, 29 | ), 30 | failureCounter: prometheus.NewCounterVec( 31 | prometheus.CounterOpts{ 32 | Namespace: namespace, 33 | Name: "reconcile_fail_total", 34 | Help: `total failed reconciles`, 35 | }, 36 | []string{"instancegroup", "reason"}, 37 | ), 38 | throttleCounter: prometheus.NewCounterVec( 39 | prometheus.CounterOpts{ 40 | Namespace: namespace, 41 | Name: "aws_api_throttle_total", 42 | Help: "number of aws API calls throttles", 43 | }, 44 | []string{"service", "operation"}, 45 | ), 46 | statusGauge: prometheus.NewGaugeVec( 47 | prometheus.GaugeOpts{ 48 | Namespace: namespace, 49 | Name: "instance_group_status", 50 | Help: "number of instance groups and their status", 51 | }, 52 | []string{"instancegroup", "status"}, 53 | ), 54 | } 55 | } 56 | 57 | func (c MetricsCollector) Collect(ch chan<- prometheus.Metric) { 58 | c.successCounter.Collect(ch) 59 | c.failureCounter.Collect(ch) 60 | c.throttleCounter.Collect(ch) 61 | c.statusGauge.Collect(ch) 62 | } 63 | 64 | func (c MetricsCollector) Describe(ch chan<- *prometheus.Desc) { 65 | c.successCounter.Describe(ch) 66 | c.failureCounter.Describe(ch) 67 | c.throttleCounter.Describe(ch) 68 | c.statusGauge.Describe(ch) 69 | } 70 | 71 | func (c *MetricsCollector) SetInstanceGroup(instanceGroup, state string) { 72 | externalStates := []string{"ReconcileModifying", "InitUpgrade", "Deleting", "Ready", "Error"} 73 | if !ContainsEqualFold(externalStates, state) { 74 | return 75 | } 76 | for _, s := range externalStates { 77 | c.statusGauge.With(prometheus.Labels{"instancegroup": instanceGroup, "status": s}).Set(0) 78 | } 79 | c.statusGauge.With(prometheus.Labels{"instancegroup": instanceGroup, "status": state}).Set(1) 80 | } 81 | 82 | func (c *MetricsCollector) UnsetInstanceGroup() { 83 | c.successCounter.Reset() 84 | c.failureCounter.Reset() 85 | c.throttleCounter.Reset() 86 | c.statusGauge.Reset() 87 | } 88 | 89 | func (c *MetricsCollector) IncSuccess(instanceGroup string) { 90 | c.successCounter.With(prometheus.Labels{"instancegroup": instanceGroup}).Inc() 91 | } 92 | 93 | func (c *MetricsCollector) IncFail(instanceGroup, reason string) { 94 | c.failureCounter.With(prometheus.Labels{"instancegroup": instanceGroup, "reason": reason}).Inc() 95 | } 96 | 97 | func (c *MetricsCollector) IncThrottle(serviceName, operationName string) { 98 | c.throttleCounter.With(prometheus.Labels{"service": serviceName, "operation": operationName}).Inc() 99 | } 100 | -------------------------------------------------------------------------------- /client/clientset/versioned/typed/instancemgr/v1alpha1/instancemgr_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "net/http" 21 | 22 | v1alpha1 "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 23 | "github.com/keikoproj/instance-manager/client/clientset/versioned/scheme" 24 | rest "k8s.io/client-go/rest" 25 | ) 26 | 27 | type InstancemgrV1alpha1Interface interface { 28 | RESTClient() rest.Interface 29 | InstanceGroupsGetter 30 | } 31 | 32 | // InstancemgrV1alpha1Client is used to interact with features provided by the instancemgr group. 33 | type InstancemgrV1alpha1Client struct { 34 | restClient rest.Interface 35 | } 36 | 37 | func (c *InstancemgrV1alpha1Client) InstanceGroups(namespace string) InstanceGroupInterface { 38 | return newInstanceGroups(c, namespace) 39 | } 40 | 41 | // NewForConfig creates a new InstancemgrV1alpha1Client for the given config. 42 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 43 | // where httpClient was generated with rest.HTTPClientFor(c). 44 | func NewForConfig(c *rest.Config) (*InstancemgrV1alpha1Client, error) { 45 | config := *c 46 | if err := setConfigDefaults(&config); err != nil { 47 | return nil, err 48 | } 49 | httpClient, err := rest.HTTPClientFor(&config) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return NewForConfigAndClient(&config, httpClient) 54 | } 55 | 56 | // NewForConfigAndClient creates a new InstancemgrV1alpha1Client for the given config and http client. 57 | // Note the http client provided takes precedence over the configured transport values. 58 | func NewForConfigAndClient(c *rest.Config, h *http.Client) (*InstancemgrV1alpha1Client, error) { 59 | config := *c 60 | if err := setConfigDefaults(&config); err != nil { 61 | return nil, err 62 | } 63 | client, err := rest.RESTClientForConfigAndClient(&config, h) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return &InstancemgrV1alpha1Client{client}, nil 68 | } 69 | 70 | // NewForConfigOrDie creates a new InstancemgrV1alpha1Client for the given config and 71 | // panics if there is an error in the config. 72 | func NewForConfigOrDie(c *rest.Config) *InstancemgrV1alpha1Client { 73 | client, err := NewForConfig(c) 74 | if err != nil { 75 | panic(err) 76 | } 77 | return client 78 | } 79 | 80 | // New creates a new InstancemgrV1alpha1Client for the given RESTClient. 81 | func New(c rest.Interface) *InstancemgrV1alpha1Client { 82 | return &InstancemgrV1alpha1Client{c} 83 | } 84 | 85 | func setConfigDefaults(config *rest.Config) error { 86 | gv := v1alpha1.SchemeGroupVersion 87 | config.GroupVersion = &gv 88 | config.APIPath = "/apis" 89 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 90 | 91 | if config.UserAgent == "" { 92 | config.UserAgent = rest.DefaultKubernetesUserAgent() 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // RESTClient returns a RESTClient that is used to communicate 99 | // with API server by this client implementation. 100 | func (c *InstancemgrV1alpha1Client) RESTClient() rest.Interface { 101 | if c == nil { 102 | return nil 103 | } 104 | return c.restClient 105 | } 106 | -------------------------------------------------------------------------------- /test-bdd/features/02_update.feature: -------------------------------------------------------------------------------- 1 | Feature: CRUD Update 2 | In order to update an instance-groups 3 | As an EKS cluster operator 4 | I need to update the custom resource 5 | 6 | Scenario: Resources can be updated 7 | Given an EKS cluster 8 | Then I update a resource instance-group.yaml with .spec.eks.minSize set to 3 9 | And I update a resource instance-group-crd.yaml with .spec.eks.minSize set to 3 10 | And I update a resource instance-group-wp.yaml with .spec.eks.minSize set to 3 11 | And I update a resource instance-group-crd-wp.yaml with .spec.eks.minSize set to 3 12 | And I update a resource instance-group-launch-template.yaml with .spec.eks.minSize set to 3 13 | And I update a resource instance-group-launch-template-mixed.yaml with .spec.eks.minSize set to 3 14 | And I update a resource instance-group-managed.yaml with .spec.eks-managed.minSize set to 3 15 | And I update a resource instance-group-latest-locked.yaml with .spec.eks.minSize set to 3 16 | 17 | Scenario: Update an instance-group with rollingUpdate strategy 18 | Given an EKS cluster 19 | When I update a resource instance-group.yaml with .spec.eks.minSize set to 3 20 | Then the resource should converge to selector .status.currentState=ready 21 | And the resource condition NodesReady should be true 22 | And 3 nodes should be ready 23 | 24 | Scenario: Update an instance-group with CRD strategy 25 | Given an EKS cluster 26 | When I update a resource instance-group-crd.yaml with .spec.eks.minSize set to 3 27 | Then the resource should converge to selector .status.currentState=ready 28 | And the resource condition NodesReady should be true 29 | And 3 nodes should be ready 30 | 31 | Scenario: Update an instance-group with rollingUpdate strategy and warm pools configured 32 | Given an EKS cluster 33 | When I update a resource instance-group-wp.yaml with .spec.eks.minSize set to 3 34 | Then the resource should converge to selector .status.currentState=ready 35 | And the resource condition NodesReady should be true 36 | And 3 nodes should be ready 37 | 38 | Scenario: Update an instance-group with CRD strategy and warm pools configured 39 | Given an EKS cluster 40 | When I update a resource instance-group-crd-wp.yaml with .spec.eks.minSize set to 3 41 | Then the resource should converge to selector .status.currentState=ready 42 | And the resource condition NodesReady should be true 43 | And 3 nodes should be ready 44 | 45 | Scenario: Update an instance-group with launch template 46 | Given an EKS cluster 47 | When I update a resource instance-group-launch-template.yaml with .spec.eks.minSize set to 3 48 | Then the resource should converge to selector .status.currentState=ready 49 | And the resource condition NodesReady should be true 50 | And 3 nodes should be ready 51 | 52 | Scenario: Update an instance-group with launch template and mixed instances 53 | Given an EKS cluster 54 | When I update a resource instance-group-launch-template-mixed.yaml with .spec.eks.minSize set to 3 55 | Then the resource should converge to selector .status.currentState=ready 56 | And the resource condition NodesReady should be true 57 | And 3 nodes should be ready 58 | 59 | Scenario: Update an instance-group with managed node-group 60 | Given an EKS cluster 61 | When I update a resource instance-group-managed.yaml with .spec.eks-managed.minSize set to 3 62 | Then the resource should converge to selector .status.currentState=ready 63 | And 3 nodes should be ready 64 | 65 | Scenario: Update an instance-group with latest ami 66 | Given an EKS cluster 67 | When I update a resource instance-group-latest-locked.yaml with .spec.eks.minSize set to 3 68 | Then the resource should converge to selector .status.currentState=ready 69 | And 3 nodes should be ready -------------------------------------------------------------------------------- /controllers/provisioners/eks/delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package eks 17 | 18 | import ( 19 | "github.com/keikoproj/instance-manager/controllers/provisioners/eks/scaling" 20 | 21 | "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 22 | kubeprovider "github.com/keikoproj/instance-manager/controllers/providers/kubernetes" 23 | "github.com/pkg/errors" 24 | 25 | "github.com/aws/aws-sdk-go/aws" 26 | ) 27 | 28 | func (ctx *EksInstanceGroupContext) Delete() error { 29 | var ( 30 | state = ctx.GetDiscoveredState() 31 | role = state.GetRole() 32 | roleARN = aws.StringValue(role.Arn) 33 | scalingConfig = state.GetScalingConfiguration() 34 | ) 35 | 36 | ctx.SetState(v1alpha1.ReconcileDeleting) 37 | // delete scaling group 38 | err := ctx.DeleteScalingGroup() 39 | if err != nil { 40 | return errors.Wrap(err, "failed to delete scaling group") 41 | } 42 | 43 | // if scaling group is deleted, remove the role from aws-auth if it's not in use by other groups 44 | if err := ctx.RemoveAuthRole(roleARN); err != nil { 45 | return errors.Wrap(err, "failed to remove auth role") 46 | } 47 | 48 | // delete launchconfig 49 | if err := scalingConfig.Delete(&scaling.DeleteConfigurationInput{ 50 | Prefix: ctx.ResourcePrefix, 51 | DeleteAll: true, 52 | }); err != nil { 53 | return errors.Wrap(err, "failed to delete launch configuration") 54 | } 55 | 56 | // delete the managed IAM role if one was created 57 | err = ctx.DeleteManagedRole() 58 | if err != nil { 59 | return errors.Wrap(err, "failed to delete scaling group role") 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (ctx *EksInstanceGroupContext) DeleteScalingGroup() error { 66 | var ( 67 | state = ctx.GetDiscoveredState() 68 | scalingGroup = state.GetScalingGroup() 69 | instanceGroup = ctx.GetInstanceGroup() 70 | asgName = aws.StringValue(scalingGroup.AutoScalingGroupName) 71 | ) 72 | 73 | if !state.HasScalingGroup() { 74 | return nil 75 | } 76 | 77 | err := ctx.AwsWorker.DeleteScalingGroup(asgName) 78 | if err != nil { 79 | return err 80 | } 81 | ctx.Log.Info("deleted scaling group", "instancegroup", instanceGroup.NamespacedName(), "scalinggroup", asgName) 82 | state.Publisher.Publish(kubeprovider.InstanceGroupDeletedEvent, "instancegroup", instanceGroup.NamespacedName(), "scalinggroup", asgName) 83 | return nil 84 | } 85 | 86 | func (ctx *EksInstanceGroupContext) DeleteManagedRole() error { 87 | var ( 88 | instanceGroup = ctx.GetInstanceGroup() 89 | configuration = instanceGroup.GetEKSConfiguration() 90 | state = ctx.GetDiscoveredState() 91 | additionalPolicies = configuration.GetManagedPolicies() 92 | role = state.GetRole() 93 | roleName = aws.StringValue(role.RoleName) 94 | ) 95 | 96 | if !state.HasRole() || configuration.HasExistingRole() { 97 | return nil 98 | } 99 | 100 | managedPolicies := ctx.GetManagedPoliciesList(additionalPolicies) 101 | 102 | err := ctx.AwsWorker.DeleteScalingGroupRole(roleName, managedPolicies) 103 | if err != nil { 104 | return err 105 | } 106 | ctx.Log.Info("deleted scaling group role", "instancegroup", instanceGroup.NamespacedName(), "iamrole", roleName) 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /client/listers/instancemgr/v1alpha1/instancegroup.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by lister-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | v1alpha1 "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 21 | "k8s.io/apimachinery/pkg/api/errors" 22 | "k8s.io/apimachinery/pkg/labels" 23 | "k8s.io/client-go/tools/cache" 24 | ) 25 | 26 | // InstanceGroupLister helps list InstanceGroups. 27 | // All objects returned here must be treated as read-only. 28 | type InstanceGroupLister interface { 29 | // List lists all InstanceGroups in the indexer. 30 | // Objects returned here must be treated as read-only. 31 | List(selector labels.Selector) (ret []*v1alpha1.InstanceGroup, err error) 32 | // InstanceGroups returns an object that can list and get InstanceGroups. 33 | InstanceGroups(namespace string) InstanceGroupNamespaceLister 34 | InstanceGroupListerExpansion 35 | } 36 | 37 | // instanceGroupLister implements the InstanceGroupLister interface. 38 | type instanceGroupLister struct { 39 | indexer cache.Indexer 40 | } 41 | 42 | // NewInstanceGroupLister returns a new InstanceGroupLister. 43 | func NewInstanceGroupLister(indexer cache.Indexer) InstanceGroupLister { 44 | return &instanceGroupLister{indexer: indexer} 45 | } 46 | 47 | // List lists all InstanceGroups in the indexer. 48 | func (s *instanceGroupLister) List(selector labels.Selector) (ret []*v1alpha1.InstanceGroup, err error) { 49 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 50 | ret = append(ret, m.(*v1alpha1.InstanceGroup)) 51 | }) 52 | return ret, err 53 | } 54 | 55 | // InstanceGroups returns an object that can list and get InstanceGroups. 56 | func (s *instanceGroupLister) InstanceGroups(namespace string) InstanceGroupNamespaceLister { 57 | return instanceGroupNamespaceLister{indexer: s.indexer, namespace: namespace} 58 | } 59 | 60 | // InstanceGroupNamespaceLister helps list and get InstanceGroups. 61 | // All objects returned here must be treated as read-only. 62 | type InstanceGroupNamespaceLister interface { 63 | // List lists all InstanceGroups in the indexer for a given namespace. 64 | // Objects returned here must be treated as read-only. 65 | List(selector labels.Selector) (ret []*v1alpha1.InstanceGroup, err error) 66 | // Get retrieves the InstanceGroup from the indexer for a given namespace and name. 67 | // Objects returned here must be treated as read-only. 68 | Get(name string) (*v1alpha1.InstanceGroup, error) 69 | InstanceGroupNamespaceListerExpansion 70 | } 71 | 72 | // instanceGroupNamespaceLister implements the InstanceGroupNamespaceLister 73 | // interface. 74 | type instanceGroupNamespaceLister struct { 75 | indexer cache.Indexer 76 | namespace string 77 | } 78 | 79 | // List lists all InstanceGroups in the indexer for a given namespace. 80 | func (s instanceGroupNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.InstanceGroup, err error) { 81 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 82 | ret = append(ret, m.(*v1alpha1.InstanceGroup)) 83 | }) 84 | return ret, err 85 | } 86 | 87 | // Get retrieves the InstanceGroup from the indexer for a given namespace and name. 88 | func (s instanceGroupNamespaceLister) Get(name string) (*v1alpha1.InstanceGroup, error) { 89 | obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) 90 | if err != nil { 91 | return nil, err 92 | } 93 | if !exists { 94 | return nil, errors.NewNotFound(v1alpha1.Resource("instancegroup"), name) 95 | } 96 | return obj.(*v1alpha1.InstanceGroup), nil 97 | } 98 | -------------------------------------------------------------------------------- /client/informers/externalversions/instancemgr/v1alpha1/instancegroup.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by informer-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "context" 21 | time "time" 22 | 23 | instancemgrv1alpha1 "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 24 | versioned "github.com/keikoproj/instance-manager/client/clientset/versioned" 25 | internalinterfaces "github.com/keikoproj/instance-manager/client/informers/externalversions/internalinterfaces" 26 | v1alpha1 "github.com/keikoproj/instance-manager/client/listers/instancemgr/v1alpha1" 27 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | runtime "k8s.io/apimachinery/pkg/runtime" 29 | watch "k8s.io/apimachinery/pkg/watch" 30 | cache "k8s.io/client-go/tools/cache" 31 | ) 32 | 33 | // InstanceGroupInformer provides access to a shared informer and lister for 34 | // InstanceGroups. 35 | type InstanceGroupInformer interface { 36 | Informer() cache.SharedIndexInformer 37 | Lister() v1alpha1.InstanceGroupLister 38 | } 39 | 40 | type instanceGroupInformer struct { 41 | factory internalinterfaces.SharedInformerFactory 42 | tweakListOptions internalinterfaces.TweakListOptionsFunc 43 | namespace string 44 | } 45 | 46 | // NewInstanceGroupInformer constructs a new informer for InstanceGroup type. 47 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 48 | // one. This reduces memory footprint and number of connections to the server. 49 | func NewInstanceGroupInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 50 | return NewFilteredInstanceGroupInformer(client, namespace, resyncPeriod, indexers, nil) 51 | } 52 | 53 | // NewFilteredInstanceGroupInformer constructs a new informer for InstanceGroup type. 54 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 55 | // one. This reduces memory footprint and number of connections to the server. 56 | func NewFilteredInstanceGroupInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 57 | return cache.NewSharedIndexInformer( 58 | &cache.ListWatch{ 59 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 60 | if tweakListOptions != nil { 61 | tweakListOptions(&options) 62 | } 63 | return client.InstancemgrV1alpha1().InstanceGroups(namespace).List(context.TODO(), options) 64 | }, 65 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 66 | if tweakListOptions != nil { 67 | tweakListOptions(&options) 68 | } 69 | return client.InstancemgrV1alpha1().InstanceGroups(namespace).Watch(context.TODO(), options) 70 | }, 71 | }, 72 | &instancemgrv1alpha1.InstanceGroup{}, 73 | resyncPeriod, 74 | indexers, 75 | ) 76 | } 77 | 78 | func (f *instanceGroupInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 79 | return NewFilteredInstanceGroupInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 80 | } 81 | 82 | func (f *instanceGroupInformer) Informer() cache.SharedIndexInformer { 83 | return f.factory.InformerFor(&instancemgrv1alpha1.InstanceGroup{}, f.defaultInformer) 84 | } 85 | 86 | func (f *instanceGroupInformer) Lister() v1alpha1.InstanceGroupLister { 87 | return v1alpha1.NewInstanceGroupLister(f.Informer().GetIndexer()) 88 | } 89 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keikoproj/instance-manager 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/Masterminds/semver v1.5.0 9 | github.com/aws/aws-sdk-go v1.55.6 10 | github.com/cucumber/godog v0.15.0 11 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 12 | github.com/ghodss/yaml v1.0.0 13 | github.com/go-logr/logr v1.4.2 14 | github.com/keikoproj/aws-auth v0.6.1 15 | github.com/keikoproj/aws-sdk-go-cache v0.1.0 16 | github.com/onsi/gomega v1.36.3 17 | github.com/pkg/errors v0.9.1 18 | github.com/prometheus/client_golang v1.22.0 19 | github.com/sirupsen/logrus v1.9.3 20 | golang.org/x/oauth2 v0.29.0 // indirect 21 | k8s.io/api v0.32.4 22 | k8s.io/apimachinery v0.32.4 23 | k8s.io/client-go v0.32.4 24 | k8s.io/code-generator v0.32.3 25 | sigs.k8s.io/controller-runtime v0.20.4 26 | ) 27 | 28 | require ( 29 | github.com/evanphx/json-patch/v5 v5.9.11 30 | golang.org/x/text v0.24.0 31 | ) 32 | 33 | require ( 34 | github.com/beorn7/perks v1.0.1 // indirect 35 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 36 | github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect 37 | github.com/cucumber/messages/go/v21 v21.0.1 // indirect 38 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 39 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 40 | github.com/fsnotify/fsnotify v1.9.0 // indirect 41 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 42 | github.com/go-logr/zapr v1.3.0 // indirect 43 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 44 | github.com/go-openapi/jsonreference v0.21.0 // indirect 45 | github.com/go-openapi/swag v0.23.1 // indirect 46 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 47 | github.com/gogo/protobuf v1.3.2 // indirect 48 | github.com/golang/glog v1.2.4 // indirect 49 | github.com/golang/protobuf v1.5.4 // indirect 50 | github.com/google/btree v1.1.3 // indirect 51 | github.com/google/gnostic-models v0.6.9 // indirect 52 | github.com/google/go-cmp v0.7.0 // indirect 53 | github.com/google/gofuzz v1.2.0 // indirect 54 | github.com/google/uuid v1.6.0 // indirect 55 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 56 | github.com/hashicorp/go-memdb v1.3.5 // indirect 57 | github.com/hashicorp/golang-lru v1.0.2 // indirect 58 | github.com/jmespath/go-jmespath v0.4.0 // indirect 59 | github.com/josharian/intern v1.0.0 // indirect 60 | github.com/jpillora/backoff v1.0.0 // indirect 61 | github.com/json-iterator/go v1.1.12 // indirect 62 | github.com/karlseguin/ccache/v2 v2.0.8 // indirect 63 | github.com/mailru/easyjson v0.9.0 // indirect 64 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 65 | github.com/modern-go/reflect2 v1.0.2 // indirect 66 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 67 | github.com/prometheus/client_model v0.6.2 // indirect 68 | github.com/prometheus/common v0.63.0 // indirect 69 | github.com/prometheus/procfs v0.16.0 // indirect 70 | github.com/spf13/pflag v1.0.6 // indirect 71 | github.com/x448/float16 v0.8.4 // indirect 72 | go.uber.org/multierr v1.11.0 // indirect 73 | go.uber.org/zap v1.27.0 // indirect 74 | golang.org/x/mod v0.24.0 // indirect 75 | golang.org/x/net v0.39.0 // indirect 76 | golang.org/x/sync v0.13.0 // indirect 77 | golang.org/x/sys v0.32.0 // indirect 78 | golang.org/x/term v0.31.0 // indirect 79 | golang.org/x/time v0.11.0 // indirect 80 | golang.org/x/tools v0.32.0 // indirect 81 | gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect 82 | google.golang.org/protobuf v1.36.6 // indirect 83 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 84 | gopkg.in/inf.v0 v0.9.1 // indirect 85 | gopkg.in/yaml.v2 v2.4.0 // indirect 86 | gopkg.in/yaml.v3 v3.0.1 // indirect 87 | k8s.io/apiextensions-apiserver v0.32.3 // indirect 88 | k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 // indirect 89 | k8s.io/klog/v2 v2.130.1 // indirect 90 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 91 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 92 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 93 | sigs.k8s.io/randfill v1.0.0 // indirect 94 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 95 | sigs.k8s.io/yaml v1.4.0 // indirect 96 | ) 97 | -------------------------------------------------------------------------------- /controllers/provisioners/eks/state_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package eks 17 | 18 | import ( 19 | "testing" 20 | "time" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | 24 | "github.com/aws/aws-sdk-go/aws" 25 | "github.com/aws/aws-sdk-go/service/autoscaling" 26 | "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 27 | "github.com/onsi/gomega" 28 | ) 29 | 30 | func TestStateDiscovery(t *testing.T) { 31 | var ( 32 | g = gomega.NewGomegaWithT(t) 33 | k = MockKubernetesClientSet() 34 | ig = MockInstanceGroup() 35 | asgMock = NewAutoScalingMocker() 36 | iamMock = NewIamMocker() 37 | eksMock = NewEksMocker() 38 | ec2Mock = NewEc2Mocker() 39 | ssmMock = NewSsmMocker() 40 | ) 41 | 42 | w := MockAwsWorker(asgMock, iamMock, eksMock, ec2Mock, ssmMock) 43 | ctx := MockContext(ig, k, w) 44 | 45 | tests := []struct { 46 | crDeleted bool 47 | scalingGroupExist bool 48 | scalingGroupDeleting bool 49 | expectedState v1alpha1.ReconcileState 50 | }{ 51 | {crDeleted: false, scalingGroupExist: false, expectedState: v1alpha1.ReconcileInitCreate}, 52 | {crDeleted: false, scalingGroupExist: true, expectedState: v1alpha1.ReconcileInitUpdate}, 53 | {crDeleted: true, scalingGroupExist: true, expectedState: v1alpha1.ReconcileInitDelete}, 54 | {crDeleted: true, scalingGroupExist: true, scalingGroupDeleting: true, expectedState: v1alpha1.ReconcileDeleting}, 55 | {crDeleted: true, scalingGroupExist: false, expectedState: v1alpha1.ReconcileDeleted}, 56 | } 57 | 58 | for i, tc := range tests { 59 | t.Logf("#%v -> %v", i, tc.expectedState) 60 | 61 | // assume initial state of init 62 | ig.SetState(v1alpha1.ReconcileInit) 63 | var deleteStatus string 64 | 65 | if tc.scalingGroupDeleting { 66 | deleteStatus = ScalingGroupDeletionStatus 67 | } 68 | if tc.crDeleted { 69 | ig.SetDeletionTimestamp(&metav1.Time{Time: time.Now()}) 70 | } 71 | ctx.SetDiscoveredState(&DiscoveredState{ 72 | Provisioned: tc.scalingGroupExist, 73 | ScalingGroup: &autoscaling.Group{ 74 | Status: aws.String(deleteStatus), 75 | }, 76 | }) 77 | ctx.StateDiscovery() 78 | g.Expect(ctx.GetState()).To(gomega.Equal(tc.expectedState)) 79 | } 80 | } 81 | 82 | func TestIsReady(t *testing.T) { 83 | var ( 84 | g = gomega.NewGomegaWithT(t) 85 | k = MockKubernetesClientSet() 86 | ig = MockInstanceGroup() 87 | asgMock = NewAutoScalingMocker() 88 | iamMock = NewIamMocker() 89 | eksMock = NewEksMocker() 90 | ec2Mock = NewEc2Mocker() 91 | ssmMock = NewSsmMocker() 92 | ) 93 | 94 | w := MockAwsWorker(asgMock, iamMock, eksMock, ec2Mock, ssmMock) 95 | ctx := MockContext(ig, k, w) 96 | 97 | tests := []struct { 98 | initialState v1alpha1.ReconcileState 99 | expectedReady bool 100 | }{ 101 | {initialState: v1alpha1.ReconcileInit, expectedReady: false}, 102 | {initialState: v1alpha1.ReconcileModified, expectedReady: true}, 103 | {initialState: v1alpha1.ReconcileErr, expectedReady: false}, 104 | {initialState: v1alpha1.ReconcileModifying, expectedReady: false}, 105 | {initialState: v1alpha1.ReconcileDeleting, expectedReady: false}, 106 | {initialState: v1alpha1.ReconcileInitCreate, expectedReady: false}, 107 | {initialState: v1alpha1.ReconcileInitUpdate, expectedReady: false}, 108 | {initialState: v1alpha1.ReconcileInitUpgrade, expectedReady: false}, 109 | } 110 | 111 | for i, tc := range tests { 112 | t.Logf("#%v -> %v", i, tc.initialState) 113 | ig.SetState(tc.initialState) 114 | ready := ctx.IsReady() 115 | g.Expect(ready).To(gomega.Equal(tc.expectedReady)) 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /test-bdd/testutil/helpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package testutil 17 | 18 | import ( 19 | "bytes" 20 | "fmt" 21 | "html/template" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | 26 | "github.com/pkg/errors" 27 | corev1 "k8s.io/api/core/v1" 28 | "k8s.io/apimachinery/pkg/api/meta" 29 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 | "k8s.io/apimachinery/pkg/runtime/schema" 31 | serializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 32 | "k8s.io/client-go/discovery" 33 | "k8s.io/client-go/discovery/cached/memory" 34 | "k8s.io/client-go/rest" 35 | "k8s.io/client-go/restmapper" 36 | ) 37 | 38 | type TemplateArguments struct { 39 | ClusterName string 40 | KeyPairName string 41 | AmiID string 42 | NodeRole string 43 | NodeRoleArn string 44 | NodeSecurityGroups []string 45 | Subnets []string 46 | } 47 | 48 | func NewTemplateArguments() *TemplateArguments { 49 | return &TemplateArguments{ 50 | ClusterName: os.Getenv("EKS_CLUSTER"), 51 | KeyPairName: os.Getenv("KEYPAIR_NAME"), 52 | AmiID: os.Getenv("AMI_ID"), 53 | NodeRole: os.Getenv("NODE_ROLE"), 54 | NodeRoleArn: os.Getenv("NODE_ROLE_ARN"), 55 | NodeSecurityGroups: strings.Split(os.Getenv("SECURITY_GROUPS"), ","), 56 | Subnets: strings.Split(os.Getenv("NODE_SUBNETS"), ","), 57 | } 58 | } 59 | 60 | func IsNodeReady(n corev1.Node) bool { 61 | for _, condition := range n.Status.Conditions { 62 | if condition.Type == "Ready" { 63 | if condition.Status == "True" { 64 | return true 65 | } 66 | } 67 | } 68 | return false 69 | } 70 | 71 | func PathToOSFile(relativePath string) (*os.File, error) { 72 | path, err := filepath.Abs(relativePath) 73 | if err != nil { 74 | return nil, errors.Wrap(err, fmt.Sprintf("failed generate absolute file path of %s", relativePath)) 75 | } 76 | 77 | manifest, err := os.Open(path) 78 | if err != nil { 79 | return nil, errors.Wrap(err, fmt.Sprintf("failed to open file %s", path)) 80 | } 81 | 82 | return manifest, nil 83 | } 84 | 85 | func DeleteEmpty(s []string) []string { 86 | var r []string 87 | for _, str := range s { 88 | if str != "" { 89 | r = append(r, str) 90 | } 91 | } 92 | return r 93 | } 94 | 95 | // find the corresponding GVR (available in *meta.RESTMapping) for gvk 96 | func FindGVR(gvk *schema.GroupVersionKind, cfg *rest.Config) (*meta.RESTMapping, error) { 97 | 98 | // DiscoveryClient queries API server about the resources 99 | dc, err := discovery.NewDiscoveryClientForConfig(cfg) 100 | if err != nil { 101 | return nil, err 102 | } 103 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc)) 104 | 105 | return mapper.RESTMapping(gvk.GroupKind(), gvk.Version) 106 | } 107 | 108 | func GetResourceFromYaml(path string, config *rest.Config, args *TemplateArguments) (*meta.RESTMapping, *unstructured.Unstructured, error) { 109 | resource := &unstructured.Unstructured{} 110 | 111 | d, err := os.ReadFile(path) 112 | if err != nil { 113 | return nil, resource, err 114 | } 115 | 116 | template, err := template.New("InstanceGroup").Parse(string(d)) 117 | if err != nil { 118 | return nil, resource, err 119 | } 120 | 121 | var renderBuffer bytes.Buffer 122 | err = template.Execute(&renderBuffer, &args) 123 | if err != nil { 124 | return nil, resource, err 125 | } 126 | dec := serializer.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) 127 | 128 | _, gvk, err := dec.Decode(renderBuffer.Bytes(), nil, resource) 129 | if err != nil { 130 | return nil, resource, err 131 | } 132 | 133 | gvr, err := FindGVR(gvk, config) 134 | if err != nil { 135 | return nil, resource, err 136 | } 137 | 138 | return gvr, resource, nil 139 | } 140 | -------------------------------------------------------------------------------- /client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | package versioned 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | 23 | instancemgrv1alpha1 "github.com/keikoproj/instance-manager/client/clientset/versioned/typed/instancemgr/v1alpha1" 24 | discovery "k8s.io/client-go/discovery" 25 | rest "k8s.io/client-go/rest" 26 | flowcontrol "k8s.io/client-go/util/flowcontrol" 27 | ) 28 | 29 | type Interface interface { 30 | Discovery() discovery.DiscoveryInterface 31 | InstancemgrV1alpha1() instancemgrv1alpha1.InstancemgrV1alpha1Interface 32 | } 33 | 34 | // Clientset contains the clients for groups. 35 | type Clientset struct { 36 | *discovery.DiscoveryClient 37 | instancemgrV1alpha1 *instancemgrv1alpha1.InstancemgrV1alpha1Client 38 | } 39 | 40 | // InstancemgrV1alpha1 retrieves the InstancemgrV1alpha1Client 41 | func (c *Clientset) InstancemgrV1alpha1() instancemgrv1alpha1.InstancemgrV1alpha1Interface { 42 | return c.instancemgrV1alpha1 43 | } 44 | 45 | // Discovery retrieves the DiscoveryClient 46 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 47 | if c == nil { 48 | return nil 49 | } 50 | return c.DiscoveryClient 51 | } 52 | 53 | // NewForConfig creates a new Clientset for the given config. 54 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 55 | // NewForConfig will generate a rate-limiter in configShallowCopy. 56 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 57 | // where httpClient was generated with rest.HTTPClientFor(c). 58 | func NewForConfig(c *rest.Config) (*Clientset, error) { 59 | configShallowCopy := *c 60 | 61 | if configShallowCopy.UserAgent == "" { 62 | configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() 63 | } 64 | 65 | // share the transport between all clients 66 | httpClient, err := rest.HTTPClientFor(&configShallowCopy) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return NewForConfigAndClient(&configShallowCopy, httpClient) 72 | } 73 | 74 | // NewForConfigAndClient creates a new Clientset for the given config and http client. 75 | // Note the http client provided takes precedence over the configured transport values. 76 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 77 | // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. 78 | func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { 79 | configShallowCopy := *c 80 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 81 | if configShallowCopy.Burst <= 0 { 82 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 83 | } 84 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 85 | } 86 | 87 | var cs Clientset 88 | var err error 89 | cs.instancemgrV1alpha1, err = instancemgrv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return &cs, nil 99 | } 100 | 101 | // NewForConfigOrDie creates a new Clientset for the given config and 102 | // panics if there is an error in the config. 103 | func NewForConfigOrDie(c *rest.Config) *Clientset { 104 | cs, err := NewForConfig(c) 105 | if err != nil { 106 | panic(err) 107 | } 108 | return cs 109 | } 110 | 111 | // New creates a new Clientset for the given RESTClient. 112 | func New(c rest.Interface) *Clientset { 113 | var cs Clientset 114 | cs.instancemgrV1alpha1 = instancemgrv1alpha1.New(c) 115 | 116 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 117 | return &cs 118 | } 119 | -------------------------------------------------------------------------------- /test-bdd/features/03_upgrade.feature: -------------------------------------------------------------------------------- 1 | Feature: CRUD Upgrade 2 | In order to rotate an instance-groups 3 | As an EKS cluster operator 4 | I need to update the custom resource instance type 5 | 6 | Scenario: Resources can be upgraded 7 | Given an EKS cluster 8 | Then I update a resource instance-group.yaml with .spec.eks.configuration.instanceType set to t2.medium 9 | And I update a resource instance-group-crd.yaml with .spec.eks.configuration.instanceType set to t2.medium 10 | And I update a resource instance-group-wp.yaml with .spec.eks.configuration.instanceType set to t2.medium 11 | And I update a resource instance-group-crd-wp.yaml with .spec.eks.configuration.instanceType set to t2.medium 12 | And I update a resource instance-group-launch-template.yaml with .spec.eks.configuration.instanceType set to t2.medium 13 | And I update a resource instance-group-launch-template-mixed.yaml with .spec.eks.configuration.instanceType set to m5.xlarge 14 | 15 | Scenario: Update an instance-group with rollingUpdate strategy 16 | Given an EKS cluster 17 | When I update a resource instance-group.yaml with .spec.eks.configuration.instanceType set to t2.medium 18 | Then 3 nodes should be ready with label beta.kubernetes.io/instance-type set to t2.medium 19 | And the resource should converge to selector .status.currentState=ready 20 | And the resource condition NodesReady should be true 21 | And 3 nodes should be ready 22 | 23 | 24 | Scenario: Update an instance-group with rollingUpdate strategy and warm pools configured 25 | Given an EKS cluster 26 | When I update a resource instance-group-wp.yaml with .spec.eks.configuration.instanceType set to t2.medium 27 | Then 3 nodes should be ready with label beta.kubernetes.io/instance-type set to t2.medium 28 | And the resource should converge to selector .status.currentState=ready 29 | And the resource condition NodesReady should be true 30 | And 3 nodes should be ready 31 | 32 | Scenario: Update an instance-group with CRD strategy and warm pools configured 33 | Given an EKS cluster 34 | When I update a resource instance-group-crd-wp.yaml with .spec.eks.configuration.instanceType set to t2.medium 35 | Then 3 nodes should be ready with label beta.kubernetes.io/instance-type set to t2.medium 36 | And the resource should converge to selector .status.currentState=ready 37 | And the resource condition NodesReady should be true 38 | And 3 nodes should be ready 39 | 40 | 41 | Scenario: Update an instance-group with launch template 42 | Given an EKS cluster 43 | When I update a resource instance-group-launch-template.yaml with .spec.eks.configuration.instanceType set to t2.medium 44 | Then 3 nodes should be ready with label beta.kubernetes.io/instance-type set to t2.medium 45 | And the resource should converge to selector .status.currentState=ready 46 | And the resource condition NodesReady should be true 47 | And 3 nodes should be ready 48 | 49 | Scenario: Update an instance-group with launch template and mixed instances 50 | Given an EKS cluster 51 | When I update a resource instance-group-launch-template-mixed.yaml with .spec.eks.configuration.instanceType set to m5.xlarge 52 | Then 3 nodes should be ready with label beta.kubernetes.io/instance-type set to m5.xlarge 53 | And the resource should converge to selector .status.currentState=ready 54 | And the resource condition NodesReady should be true 55 | And 3 nodes should be ready 56 | 57 | Scenario: Update an instance-group with CRD strategy 58 | Given an EKS cluster 59 | When I update a resource instance-group-crd.yaml with .spec.eks.configuration.instanceType set to t2.medium 60 | Then 3 nodes should be ready with label beta.kubernetes.io/instance-type set to t2.medium 61 | And the resource should converge to selector .status.currentState=ready 62 | And the resource condition NodesReady should be true 63 | And 3 nodes should be ready 64 | 65 | Scenario: Lock an instance-group 66 | Given an EKS cluster 67 | When I update a resource instance-group-latest-locked.yaml with annotation instancemgr.keikoproj.io/lock-upgrades set to true 68 | Then I update a resource instance-group-latest-locked.yaml with .spec.eks.configuration.instanceType set to t2.medium 69 | And the resource should converge to selector .status.currentState=locked 70 | And I update a resource instance-group-latest-locked.yaml with annotation instancemgr.keikoproj.io/lock-upgrades set to false 71 | And the resource should converge to selector .status.currentState=ready 72 | And the resource condition NodesReady should be true 73 | And 3 nodes should be ready 74 | -------------------------------------------------------------------------------- /docs/examples/EKS-fargate.md: -------------------------------------------------------------------------------- 1 | ### EKS Fargate 2 | 3 | The purpose of the fargate provisioner is to enable the management of Fargate profiles. 4 | 5 | By associating EKS clusters with a Fargate Profile, pods can be identified for execution through profile selectors. If a to-be-scheduled pod matches any of the selectors in the Fargate Profile, then that pod is scheduled on Fargate. 6 | 7 | An EKS cluster can have multiple Fargate Profiles. If a pod matches multiple Fargate Profiles, Amazon EKS picks one of the matches at random. 8 | 9 | EKS supports clusters with both local worker nodes and Fargate management. If a pod is scheduled and matches a Fargate selector then Fargate manages the pod. Otherwise the pod is scheduled on a worker node. Clusters can be defined without any worker nodes (0) and completely rely upon Fargate for scheduling and running pods. 10 | 11 | More on [Fargate](https://docs.aws.amazon.com/eks/latest/userguide/fargate.html). 12 | 13 | The provisioner will manage (create and delete) Fargate Profiles on any EKS cluster (within the account) regardless of whether the cluster was created via CloudFormation, the AWS CLI or the AWS API. 14 | 15 | Below is an example specification for the **eks-fargate** provisioner 16 | 17 | ```yaml 18 | apiVersion: instancemgr.keikoproj.io/v1alpha1 19 | kind: InstanceGroup 20 | metadata: 21 | name: hello-world 22 | spec: 23 | # provision for EKS using Fargate 24 | provisioner: eks-fargate 25 | strategy: 26 | type: managed 27 | # provisioner configuration 28 | eks-fargate: 29 | clusterName: "the-cluster-for-my-pods" 30 | podExecutionRoleArn: "arn:aws:iam::123456789012:role/MyPodRole" 31 | subnets: 32 | - subnet-1a2b3c4d 33 | - subnet-4d3c2b1a 34 | - subnet-0w9x8y7z 35 | selectors: 36 | - namespace1: 37 | labels: 38 | key1: "value1" 39 | key2: "value2" 40 | - namespace2: 41 | labels: 42 | key1: "value1" 43 | key2: "value2" 44 | tags: 45 | key1: "value1" 46 | key2: "value2" 47 | ``` 48 | 49 | Read more about the [Fargate Profile](https://docs.aws.amazon.com/eks/latest/userguide/fargate-profile.html). 50 | 51 | Note that the eks-fargate provisioner does not accept a Fargate profile name. Instead, the provisioner creates a unique profile name based upon the cluster name, instance group name and namespace. 52 | 53 | If the above *podExecutionRoleArn* parameter is not specified, the provisioner will create a simple, limited role and policy that enables the pod to start but not access any AWS resources. The role's name will be prefixed by the generated Fargate profile name from above. That role and policy are shown below. 54 | 55 | ```yaml 56 | Type: 'AWS::IAM::Role' 57 | Properties: 58 | AssumeRolePolicyDocument: 59 | Version: 2012-10-17 60 | Statement: 61 | - Effect: "Allow" 62 | Principal: 63 | Service: "eks-fargate-pods.amazonaws.com" 64 | Action: "sts:AssumeRole" 65 | ManagedPolicyArns: 66 | - "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy" 67 | Path: / 68 | ``` 69 | 70 | Most likely an execution role with access to addtional AWS resources will be required. In this case, the above IAM role can be used as the basis to create a new, custom role with the IAM policies specific to your pods. Create your new role and your pod specific policies and use the new role's ARN as the *podExecutionRoleArn* parameter value in eks-fargate spec. 71 | 72 | Here is an example of a role with an additional policy for S3 access. 73 | 74 | ```yaml 75 | Type: 'AWS::IAM::Role' 76 | Properties: 77 | AssumeRolePolicyDocument: 78 | Version: 2012-10-17 79 | Statement: 80 | - Effect: "Allow" 81 | Principal: 82 | Service: "eks-fargate-pods.amazonaws.com" 83 | Action: "sts:AssumeRole" 84 | ManagedPolicyArns: 85 | - "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy" 86 | Path: / 87 | Policies: 88 | - PolicyName: "your chosen name" 89 | PolicyDocument: 90 | Version: 2012-10-17 91 | Statement: 92 | - Effect: Allow 93 | Action: 94 | - 's3:*' 95 | Resource: '*' 96 | ``` 97 | 98 | AWS's Fargate Profiles are immutable. Once one is created, it cannot be directly modified. It first has to be deleted and then re-created with the desired change. 99 | 100 | The **eks-fargate** provisioner is built on top of that immutability. Therefore, if an attempt is made to modify an existing profile, the provisioner will return an error. You first have to `delete` the profile and follow that with a `create`. 101 | -------------------------------------------------------------------------------- /test-bdd/features/01_create.feature: -------------------------------------------------------------------------------- 1 | Feature: CRUD Create 2 | In order to create instance-groups 3 | As an EKS cluster operator 4 | I need to submit the custom resource 5 | 6 | Scenario: Resources can be submitted 7 | Given an EKS cluster 8 | Then I create a resource namespace.yaml 9 | And I create a resource namespace-gitops.yaml 10 | And I create a resource instance-group.yaml 11 | And I create a resource instance-group-crd.yaml 12 | And I create a resource instance-group-wp.yaml 13 | And I create a resource instance-group-crd-wp.yaml 14 | And I create a resource instance-group-managed.yaml 15 | And I create a resource instance-group-fargate.yaml 16 | And I create a resource instance-group-launch-template.yaml 17 | And I create a resource instance-group-launch-template-mixed.yaml 18 | And I create a resource manager-configmap.yaml 19 | And I create a resource instance-group-gitops.yaml 20 | And I create a resource instance-group-latest-locked.yaml 21 | 22 | Scenario: Create an instance-group with rollingUpdate strategy 23 | Given an EKS cluster 24 | When I create a resource instance-group.yaml 25 | Then the resource should be created 26 | And the resource should converge to selector .status.currentState=ready 27 | And the resource condition NodesReady should be true 28 | And 2 nodes should be ready 29 | 30 | Scenario: Create an instance-group with CRD strategy 31 | Given an EKS cluster 32 | When I create a resource instance-group-crd.yaml 33 | Then the resource should be created 34 | And the resource should converge to selector .status.currentState=ready 35 | And the resource condition NodesReady should be true 36 | And 2 nodes should be ready 37 | 38 | Scenario: Create an instance-group with rollingUpdate strategy and warm pools configured 39 | Given an EKS cluster 40 | When I create a resource instance-group-wp.yaml 41 | Then the resource should be created 42 | And the resource should converge to selector .status.currentState=ready 43 | And the resource condition NodesReady should be true 44 | And 2 nodes should be ready 45 | 46 | Scenario: Create an instance-group with CRD strategy and warm pools configured 47 | Given an EKS cluster 48 | When I create a resource instance-group-crd-wp.yaml 49 | Then the resource should be created 50 | And the resource should converge to selector .status.currentState=ready 51 | And the resource condition NodesReady should be true 52 | And 2 nodes should be ready 53 | 54 | Scenario: Create an instance-group with launch template 55 | Given an EKS cluster 56 | When I create a resource instance-group-launch-template.yaml 57 | Then the resource should be created 58 | And the resource should converge to selector .status.currentState=ready 59 | And the resource condition NodesReady should be true 60 | And 2 nodes should be ready 61 | 62 | Scenario: Create an instance-group with launch template and mixed instances 63 | Given an EKS cluster 64 | When I create a resource instance-group-launch-template-mixed.yaml 65 | Then the resource should be created 66 | And the resource should converge to selector .status.currentState=ready 67 | And the resource condition NodesReady should be true 68 | And 2 nodes should be ready 69 | 70 | Scenario: Create an instance-group with managed node group 71 | Given an EKS cluster 72 | When I create a resource instance-group-managed.yaml 73 | Then the resource should be created 74 | And the resource should converge to selector .status.currentState=ready 75 | And 2 nodes should be ready 76 | 77 | Scenario: Create a fargate profile with default execution role 78 | Given an EKS cluster 79 | Then I create a resource instance-group-fargate.yaml 80 | And the resource should be created 81 | And the resource should converge to selector .status.currentState=ready 82 | 83 | Scenario: Create an instance-group with shortened resource 84 | Given an EKS cluster 85 | When I create a resource instance-group-gitops.yaml 86 | Then the resource should be created 87 | And the resource should converge to selector .status.currentState=ready 88 | And the resource condition NodesReady should be true 89 | And 2 nodes should be ready 90 | 91 | Scenario: Create an instance-group with latest ami 92 | Given an EKS cluster 93 | When I create a resource instance-group-latest-locked.yaml 94 | Then the resource should be created 95 | And the resource should converge to selector .status.currentState=ready 96 | And the resource condition NodesReady should be true 97 | And 2 nodes should be ready 98 | -------------------------------------------------------------------------------- /controllers/providers/aws/predicates.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package aws 17 | 18 | import ( 19 | "github.com/aws/aws-sdk-go/aws" 20 | "github.com/aws/aws-sdk-go/service/autoscaling" 21 | "github.com/aws/aws-sdk-go/service/eks" 22 | ) 23 | 24 | func IsUsingLaunchConfiguration(group *autoscaling.Group) bool { 25 | return group.LaunchConfigurationName != nil 26 | } 27 | 28 | func IsUsingLaunchTemplate(group *autoscaling.Group) bool { 29 | return group.LaunchTemplate != nil && group.LaunchTemplate.LaunchTemplateName != nil 30 | } 31 | 32 | type ManagedNodeGroupReconcileState struct { 33 | OngoingState bool 34 | FiniteState bool 35 | UnrecoverableError bool 36 | UnrecoverableDeleteError bool 37 | } 38 | 39 | var ManagedNodeGroupOngoingState = ManagedNodeGroupReconcileState{OngoingState: true} 40 | var ManagedNodeGroupFiniteState = ManagedNodeGroupReconcileState{FiniteState: true} 41 | var ManagedNodeGroupUnrecoverableError = ManagedNodeGroupReconcileState{UnrecoverableError: true} 42 | var ManagedNodeGroupUnrecoverableDeleteError = ManagedNodeGroupReconcileState{UnrecoverableDeleteError: true} 43 | 44 | func IsNodeGroupInConditionState(key string, condition string) bool { 45 | conditionStates := map[string]ManagedNodeGroupReconcileState{ 46 | "CREATING": ManagedNodeGroupOngoingState, 47 | "UPDATING": ManagedNodeGroupOngoingState, 48 | "DELETING": ManagedNodeGroupOngoingState, 49 | "ACTIVE": ManagedNodeGroupFiniteState, 50 | "DEGRADED": ManagedNodeGroupFiniteState, 51 | "CREATE_FAILED": ManagedNodeGroupUnrecoverableError, 52 | "DELETE_FAILED": ManagedNodeGroupUnrecoverableDeleteError, 53 | } 54 | state := conditionStates[key] 55 | 56 | switch condition { 57 | case "OngoingState": 58 | return state.OngoingState 59 | case "FiniteState": 60 | return state.FiniteState 61 | case "UnrecoverableError": 62 | return state.UnrecoverableError 63 | case "UnrecoverableDeleteError": 64 | return state.UnrecoverableDeleteError 65 | default: 66 | return false 67 | } 68 | } 69 | 70 | type CloudResourceReconcileState struct { 71 | OngoingState bool 72 | FiniteState bool 73 | FiniteDeleted bool 74 | UpdateRecoverableError bool 75 | UnrecoverableError bool 76 | UnrecoverableDeleteError bool 77 | } 78 | 79 | var OngoingState = CloudResourceReconcileState{OngoingState: true} 80 | var FiniteState = CloudResourceReconcileState{FiniteState: true} 81 | var FiniteDeleted = CloudResourceReconcileState{FiniteDeleted: true} 82 | var UpdateRecoverableError = CloudResourceReconcileState{UpdateRecoverableError: true} 83 | var UnrecoverableError = CloudResourceReconcileState{UnrecoverableError: true} 84 | var UnrecoverableDeleteError = CloudResourceReconcileState{UnrecoverableDeleteError: true} 85 | 86 | func IsProfileInConditionState(key string, condition string) bool { 87 | 88 | conditionStates := map[string]CloudResourceReconcileState{ 89 | aws.StringValue(nil): FiniteDeleted, 90 | eks.FargateProfileStatusCreating: OngoingState, 91 | eks.FargateProfileStatusActive: FiniteState, 92 | eks.FargateProfileStatusDeleting: OngoingState, 93 | eks.FargateProfileStatusCreateFailed: UpdateRecoverableError, 94 | eks.FargateProfileStatusDeleteFailed: UnrecoverableDeleteError, 95 | } 96 | state := conditionStates[key] 97 | switch condition { 98 | case "OngoingState": 99 | return state.OngoingState 100 | case "FiniteState": 101 | return state.FiniteState 102 | case "FiniteDeleted": 103 | return state.FiniteDeleted 104 | case "UpdateRecoverableError": 105 | return state.UpdateRecoverableError 106 | case "UnrecoverableError": 107 | return state.UnrecoverableError 108 | case "UnrecoverableDeleteError": 109 | return state.UnrecoverableDeleteError 110 | default: 111 | return false 112 | } 113 | } 114 | 115 | func IsUsingMixedInstances(group *autoscaling.Group) bool { 116 | return group.MixedInstancesPolicy != nil 117 | } 118 | 119 | func IsUsingWarmPool(group *autoscaling.Group) bool { 120 | return group.WarmPoolConfiguration != nil 121 | } 122 | -------------------------------------------------------------------------------- /controllers/providers/kubernetes/events.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package kubernetes 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "math/rand" 23 | "time" 24 | 25 | "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 26 | "k8s.io/apimachinery/pkg/types" 27 | 28 | v1 "k8s.io/api/core/v1" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/client-go/kubernetes" 31 | ) 32 | 33 | // EventKind defines the kind of an event 34 | type EventKind string 35 | 36 | // EventLevel defines the level of an event 37 | type EventLevel string 38 | 39 | var ( 40 | // InvolvedObjectKind is the default kind of involved objects 41 | InvolvedObjectKind = "InstanceGroup" 42 | // EventName is the default name for service events 43 | EventControllerName = "instance-manager" 44 | // EventLevelNormal is the level of a normal event 45 | EventLevelNormal = "Normal" 46 | // EventLevelWarning is the level of a warning event 47 | EventLevelWarning = "Warning" 48 | 49 | InstanceGroupCreatedEvent EventKind = "InstanceGroupCreated" 50 | InstanceGroupDeletedEvent EventKind = "InstanceGroupDeleted" 51 | NodesReadyEvent EventKind = "InstanceGroupNodesReady" 52 | NodesNotReadyEvent EventKind = "InstanceGroupNodesNotReady" 53 | InstanceGroupUpgradeFailedEvent EventKind = "InstanceGroupUpgradeFailed" 54 | 55 | EventLevels = map[EventKind]string{ 56 | InstanceGroupCreatedEvent: EventLevelNormal, 57 | InstanceGroupDeletedEvent: EventLevelNormal, 58 | NodesNotReadyEvent: EventLevelWarning, 59 | NodesReadyEvent: EventLevelNormal, 60 | InstanceGroupUpgradeFailedEvent: EventLevelWarning, 61 | } 62 | 63 | EventMessages = map[EventKind]string{ 64 | InstanceGroupCreatedEvent: "instance group has been successfully created", 65 | InstanceGroupDeletedEvent: "instance group has been successfully deleted", 66 | InstanceGroupUpgradeFailedEvent: "instance group has failed upgrading", 67 | NodesNotReadyEvent: "instance group nodes are not ready", 68 | NodesReadyEvent: "instance group nodes are ready", 69 | } 70 | ) 71 | 72 | type EventPublisher struct { 73 | Client kubernetes.Interface 74 | Name string 75 | Namespace string 76 | UID types.UID 77 | ResourceVersion string 78 | } 79 | 80 | func (e *EventPublisher) Publish(kind EventKind, keysAndValues ...interface{}) { 81 | 82 | messageFields := make(map[string]string) 83 | messageFields["msg"] = getEventMessage(kind) 84 | 85 | for i := 0; i < len(keysAndValues); i += 2 { 86 | key := keysAndValues[i].(string) 87 | value := keysAndValues[i+1].(string) 88 | messageFields[key] = value 89 | } 90 | 91 | payload, err := json.Marshal(messageFields) 92 | if err != nil { 93 | log.Error(err, "failed to marshal event message fields", "fields", messageFields) 94 | } 95 | 96 | now := time.Now() 97 | 98 | eventName := fmt.Sprintf("%v.%v.%v", EventControllerName, time.Now().Unix(), rand.Int()) 99 | event := &v1.Event{ 100 | ObjectMeta: metav1.ObjectMeta{ 101 | Name: eventName, 102 | Namespace: e.Namespace, 103 | }, 104 | InvolvedObject: v1.ObjectReference{ 105 | Kind: InvolvedObjectKind, 106 | Namespace: e.Namespace, 107 | Name: e.Name, 108 | APIVersion: v1alpha1.GroupVersion.Version, 109 | UID: e.UID, 110 | ResourceVersion: e.ResourceVersion, 111 | }, 112 | Reason: string(kind), 113 | Message: string(payload), 114 | Type: getEventLevel(kind), 115 | FirstTimestamp: metav1.NewTime(now), 116 | LastTimestamp: metav1.NewTime(now), 117 | } 118 | 119 | _, err = e.Client.CoreV1().Events(e.Namespace).Create(context.Background(), event, metav1.CreateOptions{}) 120 | if err != nil { 121 | log.Error(err, "failed to publish event", "event", event) 122 | } 123 | } 124 | 125 | func getEventLevel(kind EventKind) string { 126 | if val, ok := EventLevels[kind]; ok { 127 | return val 128 | } 129 | return EventLevelNormal 130 | } 131 | 132 | func getEventMessage(kind EventKind) string { 133 | if val, ok := EventMessages[kind]; ok { 134 | return val 135 | } 136 | return EventLevelNormal 137 | } 138 | -------------------------------------------------------------------------------- /controllers/reconcilers_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package controllers 16 | 17 | import ( 18 | "sync" 19 | "testing" 20 | 21 | v1alpha1 "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 22 | "github.com/keikoproj/instance-manager/controllers/common" 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 28 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 29 | ) 30 | 31 | func init() { 32 | // Setup logging for tests 33 | ctrl.SetLogger(zap.New(zap.UseDevMode(true))) 34 | } 35 | 36 | // Helper function to create a properly initialized reconciler for testing 37 | func createTestReconciler(objs ...runtime.Object) *InstanceGroupReconciler { 38 | s := runtime.NewScheme() 39 | _ = v1alpha1.AddToScheme(s) 40 | _ = corev1.AddToScheme(s) 41 | 42 | fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() 43 | 44 | reconciler := &InstanceGroupReconciler{ 45 | Client: fakeClient, 46 | Log: ctrl.Log.WithName("controllers").WithName("InstanceGroup"), 47 | MaxParallel: 10, 48 | NodeRelabel: true, 49 | Namespaces: make(map[string]corev1.Namespace), 50 | NamespacesLock: &sync.RWMutex{}, 51 | ConfigRetention: 100, 52 | Metrics: common.NewMetricsCollector(), 53 | } 54 | return reconciler 55 | } 56 | 57 | func TestSpotEventReconciler(t *testing.T) { 58 | // Create a test instancegroup 59 | instanceGroup := &v1alpha1.InstanceGroup{ 60 | ObjectMeta: metav1.ObjectMeta{ 61 | Name: "test-ig", 62 | Namespace: "default", 63 | }, 64 | Spec: v1alpha1.InstanceGroupSpec{ 65 | // Create a basic spec without invalid fields 66 | }, 67 | } 68 | 69 | reconciler := createTestReconciler(instanceGroup) 70 | 71 | // Create a spot termination event 72 | event := &corev1.Event{ 73 | ObjectMeta: metav1.ObjectMeta{ 74 | Name: "spot-termination", 75 | Namespace: "default", 76 | }, 77 | Reason: "SpotInterruption", 78 | InvolvedObject: corev1.ObjectReference{ 79 | Kind: "Node", 80 | Name: "test-node", 81 | Namespace: "default", 82 | }, 83 | } 84 | 85 | // Test the reconciler 86 | requests := reconciler.spotEventReconciler(event) 87 | 88 | // No requests expected in this test case since we don't have spot instances set up properly 89 | if len(requests) != 0 { 90 | t.Errorf("Expected 0 requests, got %d", len(requests)) 91 | } 92 | } 93 | 94 | func TestNodeReconciler(t *testing.T) { 95 | // Create a test node 96 | node := &corev1.Node{ 97 | ObjectMeta: metav1.ObjectMeta{ 98 | Name: "test-node", 99 | Labels: map[string]string{ 100 | "instancegroups.keikoproj.io/instance-group-name": "test-ig", 101 | }, 102 | }, 103 | } 104 | 105 | reconciler := createTestReconciler(node) 106 | 107 | // Test the reconciler 108 | requests := reconciler.nodeReconciler(node) 109 | 110 | // Expecting no requests since we don't have actual instancegroups in the test client 111 | if len(requests) != 0 { 112 | t.Errorf("Expected 0 requests, got %d", len(requests)) 113 | } 114 | } 115 | 116 | func TestConfigMapReconciler(t *testing.T) { 117 | // Create a test configmap 118 | cm := &corev1.ConfigMap{ 119 | ObjectMeta: metav1.ObjectMeta{ 120 | Name: "scaling-configuration", 121 | Namespace: "default", 122 | }, 123 | Data: map[string]string{ 124 | "test-instancegroup": "some-data", 125 | }, 126 | } 127 | 128 | reconciler := createTestReconciler(cm) 129 | 130 | // Test the reconciler 131 | requests := reconciler.configMapReconciler(cm) 132 | 133 | // Expecting no requests since we don't have actual instancegroups in the test client 134 | if len(requests) != 0 { 135 | t.Errorf("Expected 0 requests, got %d", len(requests)) 136 | } 137 | } 138 | 139 | func TestNamespaceReconciler(t *testing.T) { 140 | // Create a test namespace 141 | ns := &corev1.Namespace{ 142 | ObjectMeta: metav1.ObjectMeta{ 143 | Name: "test-namespace", 144 | }, 145 | } 146 | 147 | reconciler := createTestReconciler(ns) 148 | 149 | // Test the reconciler 150 | requests := reconciler.namespaceReconciler(ns) 151 | 152 | // Expecting no requests since we don't have actual instancegroups in the test client 153 | if len(requests) != 0 { 154 | t.Errorf("Expected 0 requests, got %d", len(requests)) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /.github/DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Development reference 2 | 3 | This document will walk you through setting up a basic testing environment, running unit tests or e2e functional tests. 4 | 5 | ## Running locally 6 | 7 | Using the `Makefile` you can use `make run` to run instance-manager locally on your machine, and it will try to reconcile InstanceGroups in the cluster - if you do this, make sure another controller is not running in your cluster already to avoid conflict. 8 | 9 | Make sure you have AWS credentials and a region exported so that your local instance-manager controller can make the required API calls. 10 | 11 | ### Example 12 | 13 | ```bash 14 | $ kubectl scale deployment instance-manager --replicas 0 15 | deployment.extensions/instance-manager scaled 16 | 17 | $ make run 18 | go fmt ./... 19 | go vet ./... 20 | go run ./main.go 21 | 2020-05-12T01:43:05.970-0700 INFO controller-runtime.metrics metrics server is starting to listen {"addr": ":8080"} 22 | 2020-05-12T01:43:06.047-0700 INFO setup starting manager 23 | 2020-05-12T01:43:06.047-0700 INFO controller-runtime.manager starting metrics server {"path": "/metrics"} 24 | 2020-05-12T01:43:06.047-0700 INFO controller-runtime.controller Starting EventSource {"controller": "instancegroup", "source": "kind source: /, Kind="} 25 | 2020-05-12T01:43:06.150-0700 INFO controller-runtime.controller Starting EventSource {"controller": "instancegroup", "source": "kind source: /, Kind="} 26 | 2020-05-12T01:43:06.255-0700 INFO controller-runtime.controller Starting Controller {"controller": "instancegroup"} 27 | 2020-05-12T01:43:06.255-0700 INFO controller-runtime.controller Starting workers {"controller": "instancegroup", "worker count": 5} 28 | ``` 29 | 30 | ## Running unit tests 31 | 32 | Using the `Makefile` you can run basic unit tests. 33 | 34 | ### Example 35 | 36 | ```bash 37 | $ make test 38 | go fmt ./... 39 | go vet ./... 40 | /Users/eibissror/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=instance-manager webhook paths="./api/...;./controllers/..." output:crd:artifacts:config=config/crd/bases 41 | go test -v ./controllers/... -coverprofile coverage.txt 42 | ? github.com/keikoproj/instance-manager/controllers [no test files] 43 | ? github.com/keikoproj/instance-manager/controllers/common [no test files] 44 | ? github.com/keikoproj/instance-manager/controllers/providers/aws [no test files] 45 | ? github.com/keikoproj/instance-manager/controllers/providers/kubernetes [no test files] 46 | ? github.com/keikoproj/instance-manager/controllers/provisioners [no test files] 47 | PASS 48 | coverage: 86.5% of statements 49 | ok github.com/keikoproj/instance-manager/controllers/provisioners/eks 0.472s coverage: 86.5% of statements 50 | coverage: 81.0% of statements 51 | ok github.com/keikoproj/instance-manager/controllers/provisioners/eksmanaged 0.785s coverage: 81.0% of statements 52 | ``` 53 | 54 | You can also run `make coverage` to generate a coverage report. 55 | 56 | ## Running BDD tests 57 | 58 | ### Dependencies 59 | 60 | 1. You will need an existing EKS cluster running with the connection details exported into a kube config file. 61 | 2. [Keikoproj Minion-Manager](https://github.com/keikoproj/minion-manager) must also be running in the cluster 62 | 3. Instance Manager needs to be started outside of the bdd test suite 63 | 64 | 65 | Export some variables and run `make bdd` to run a functional e2e test. 66 | 67 | ### Example 68 | 69 | ```bash 70 | export AWS_REGION=us-west-2 71 | export KUBECONFIG=~/.kube/config 72 | 73 | export EKS_CLUSTER=my-eks-cluster 74 | export KEYPAIR_NAME=MyKeyPair 75 | export AMI_ID=ami-EXAMPLEdk93 76 | export SECURITY_GROUPS=sg-EXAMPLE2323,sg-EXAMPLE4433 77 | export NODE_SUBNETS=subnet-EXAMPLE223d,subnet-EXAMPLEdkkf,subnet-EXAMPLEkkr9 78 | 79 | # an existing role for nodes 80 | export NODE_ROLE_ARN=arn:aws:iam::123456789012:role/basic-eks-role 81 | export NODE_ROLE=basic-eks-role 82 | 83 | $ make bdd 84 | 85 | Feature: CRUD Create 86 | In order to create instance-groups 87 | As an EKS cluster operator 88 | I need to submit the custom resource 89 | 90 | Scenario: Resources can be submitted # features/01_create.feature:6 91 | Given an EKS cluster # main_test.go:125 -> *FunctionalTest 92 | Then I create a resource instance-group.yaml # main_test.go:165 -> *FunctionalTest 93 | And I create a resource instance-group-crd.yaml # main_test.go:165 -> *FunctionalTest 94 | And I create a resource instance-group-managed.yaml # main_test.go:165 -> *FunctionalTest 95 | 96 | ... 97 | ... 98 | 99 | 15 scenarios (15 passed) 100 | 72 steps (72 passed) 101 | 22m40.347700419s 102 | testing: warning: no tests to run 103 | PASS 104 | ok github.com/keikoproj/instance-manager/test-bdd 1362.336s [no tests to run] 105 | ``` 106 | 107 | Note: If your test cluster uses `InstanceGroups` to run core components, annotating the namespace with `instancemgr.keikoproj.io/config-excluded="true"` can help prevent unexpected disruption. 108 | -------------------------------------------------------------------------------- /.github/workflows/image-push.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish image 2 | permissions: 3 | contents: write # Needed to check out the repository and update releases 4 | packages: write # Needed to push images to GitHub Container Registry (ghcr.io) 5 | attestations: write # For generating attestations 6 | id-token: write # For OIDC token authentication 7 | 8 | on: 9 | push: 10 | branches: [ master ] 11 | tags: 12 | - "v*.*.*" 13 | 14 | jobs: 15 | build-and-push: 16 | name: Build and push image 17 | runs-on: ubuntu-latest 18 | if: github.event_name != 'pull_request' 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 # Fetch all history for proper versioning 24 | 25 | # Set up QEMU for multi-platform builds 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v3 28 | 29 | # Set up Docker Buildx 30 | - name: Set up Docker Buildx 31 | uses: docker/setup-buildx-action@v3 32 | 33 | # Extract metadata for Docker 34 | - name: Extract Docker metadata 35 | id: meta 36 | uses: docker/metadata-action@v5 37 | with: 38 | images: docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }},ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} 39 | tags: | 40 | type=semver,pattern={{version}} 41 | type=semver,pattern={{major}}.{{minor}} 42 | type=ref,event=branch 43 | env: 44 | DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index 45 | 46 | # Extract primary Docker tag (without 'v') 47 | - name: Extract primary Docker tag 48 | id: docker_tag 49 | run: | 50 | TAGS="${{ steps.meta.outputs.tags }}" 51 | IFS=$'\n' read -r FIRST_IMAGE <<< "$TAGS" 52 | PRIMARY_TAG="${FIRST_IMAGE##*:}" 53 | echo "tag=$PRIMARY_TAG" >> $GITHUB_OUTPUT 54 | 55 | # Login to DockerHub 56 | - name: Login to DockerHub 57 | uses: docker/login-action@v3 58 | with: 59 | username: ${{ secrets.DOCKERHUB_USERNAME }} 60 | password: ${{ secrets.DOCKERHUB_TOKEN }} 61 | 62 | # Login to GitHub Container Registry 63 | - name: Login to GHCR 64 | uses: docker/login-action@v3 65 | with: 66 | registry: ghcr.io 67 | username: ${{ github.actor }} 68 | password: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | - name: Build and push cross-platform image 71 | id: push 72 | uses: docker/build-push-action@v6 73 | with: 74 | context: . 75 | file: ./Dockerfile 76 | platforms: linux/amd64,linux/arm64 77 | push: true 78 | provenance: false 79 | tags: ${{ steps.meta.outputs.tags }} 80 | labels: ${{ steps.meta.outputs.labels }} 81 | annotations: ${{ steps.meta.outputs.annotations }} 82 | cache-from: type=gha 83 | cache-to: type=gha,mode=max 84 | build-args: | 85 | CREATED=${{ github.event.head_commit.timestamp || format('{0:yyyy-MM-ddTHH:mm:ssZ}', github.event.repository.updated_at) }} 86 | VERSION=${{ github.ref_name }} 87 | REVISION=${{ github.sha }} 88 | 89 | - name: Generate artifact attestation (dockerhub) 90 | uses: actions/attest-build-provenance@v2 91 | with: 92 | subject-name: docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }} 93 | subject-digest: ${{ steps.push.outputs.digest }} 94 | push-to-registry: true 95 | 96 | - name: Generate artifact attestation (ghcr) 97 | uses: actions/attest-build-provenance@v2 98 | with: 99 | subject-name: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} 100 | subject-digest: ${{ steps.push.outputs.digest }} 101 | push-to-registry: true 102 | 103 | - name: Update GitHub Release with image and attestation links 104 | if: startsWith(github.ref, 'refs/tags/v') 105 | uses: softprops/action-gh-release@v2 106 | with: 107 | tag_name: ${{ github.ref_name }} 108 | append_body: true 109 | body: | 110 | ## Docker Images 111 | - [DockerHub](https://hub.docker.com/r/${{ github.repository_owner }}/${{ github.event.repository.name }}/tags?name=${{ steps.docker_tag.outputs.tag }}) 112 | - [GHCR](https://github.com/orgs/${{ github.repository_owner }}/pkgs/container/${{ github.event.repository.name }}) 113 | - `docker pull ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ steps.docker_tag.outputs.tag }}` 114 | - `docker pull ${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ steps.docker_tag.outputs.tag }}` 115 | 116 | ## Attestations 117 | - DockerHub attestation for `${{ steps.docker_tag.outputs.tag }}` published (see OCI provenance) 118 | - GHCR attestation for `${{ steps.docker_tag.outputs.tag }}` published (see OCI provenance) 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export GO111MODULE=on 2 | 3 | CONTROLLER_GEN_VERSION := v0.17.2 4 | GO_MIN_VERSION := 12000 # go1.20 5 | 6 | define generate_int_from_semver 7 | echo $(1) |cut -dv -f2 |awk '{split($$0,a,"."); print a[3]+(100*a[2])+(10000* a[1])}' 8 | endef 9 | 10 | CONTROLLER_GEN_VERSION_CHECK = \ 11 | $(shell expr \ 12 | $(shell $(call generate_int_from_semver,$(shell $(CONTROLLER_GEN) --version | awk '{print $$2}' | cut -dv -f2))) \ 13 | \>= $(shell $(call generate_int_from_semver,$(shell echo $(CONTROLLER_GEN_VERSION) | cut -dv -f2))) \ 14 | ) 15 | 16 | GO_VERSION_CHECK := \ 17 | $(shell expr \ 18 | $(shell go version | \ 19 | awk '{print $$3}' | \ 20 | cut -do -f2 | \ 21 | sed -e 's/\.\([0-9][0-9]\)/\1/g' -e 's/\.\([0-9]\)/0\1/g' -e 's/^[0-9]\{3,4\}$$/&00/' \ 22 | ) \>= $(GO_MIN_VERSION) \ 23 | ) 24 | 25 | # Default Go linker flags. 26 | GO_LDFLAGS ?= -ldflags="-s -w" 27 | 28 | # Image URL to use all building/pushing image targets 29 | IMG ?= instance-manager:latest 30 | GIT_COMMIT := $(shell git rev-parse HEAD) 31 | GIT_SHORT_SHA := $(shell git rev-parse --short HEAD) 32 | GIT_TAG := $(shell git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") 33 | GIT_DIRTY := $(shell test -n "`git status --porcelain`" && echo "-dirty" || echo "") 34 | INSTANCEMGR_TAG ?= $(GIT_TAG)-$(GIT_SHORT_SHA)$(GIT_DIRTY) 35 | BUILD_DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ') 36 | 37 | .PHONY: all 38 | all: check-go lint test clean manager 39 | 40 | # Run tests 41 | .PHONY: test 42 | test: generate fmt vet manifests 43 | go test ./controllers/... ./api/... -coverprofile coverage.txt 44 | 45 | .PHONY: bdd 46 | bdd: 47 | go test -timeout 60m -v ./test-bdd/ --godog.stop-on-failure 48 | 49 | .PHONY: wip 50 | wip: 51 | go test -timeout 60m -v ./test-bdd/ --godog.tags "@wip" 52 | 53 | .PHONY: coverage 54 | coverage: 55 | go test -coverprofile coverage.txt -v ./controllers/... 56 | go tool cover -html=coverage.txt -o coverage.html 57 | 58 | # Build manager binary 59 | .PHONY: manager 60 | manager: generate fmt vet 61 | go build -o bin/manager main.go 62 | 63 | # Run against the configured Kubernetes cluster in ~/.kube/config 64 | .PHONY: run 65 | run: generate fmt vet 66 | go run ./main.go 67 | 68 | # Install CRDs into a cluster 69 | .PHONY: install 70 | install: manifests 71 | kubectl apply -f config/rbac/service_account.yaml 72 | kubectl auth reconcile -f config/rbac/role.yaml 73 | kubectl auth reconcile -f config/rbac/strategy_role.yaml 74 | kubectl auth reconcile -f config/rbac/role_binding.yaml 75 | kubectl auth reconcile -f config/rbac/strategy_role_binding.yaml 76 | kubectl apply -f config/crd/bases 77 | 78 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 79 | .PHONY: deploy 80 | deploy: manifests 81 | kubectl apply -f config/crd/bases 82 | kustomize build config/default | kubectl apply -f - 83 | 84 | # Generate manifests e.g. CRD, RBAC etc. 85 | .PHONY: manifests 86 | manifests: controller-gen 87 | $(CONTROLLER_GEN) rbac:roleName=instance-manager crd webhook paths="./api/...;./controllers/..." output:crd:artifacts:config=config/crd/bases 88 | 89 | # Run go fmt against code 90 | .PHONY: fmt 91 | fmt: 92 | go fmt ./... 93 | 94 | # Run go vet against code 95 | .PHONY: vet 96 | vet: 97 | go vet ./... 98 | 99 | # Generate code 100 | .PHONY: generate 101 | generate: controller-gen 102 | $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./api/... 103 | 104 | # Build the docker image 105 | .PHONY: docker-build 106 | docker-build: 107 | docker build . -t ${IMG} \ 108 | --build-arg CREATED=$(BUILD_DATE) \ 109 | --build-arg VERSION=$(INSTANCEMGR_TAG) \ 110 | --label "org.opencontainers.image.created=$(BUILD_DATE)" \ 111 | --label "org.opencontainers.image.version=$(INSTANCEMGR_TAG)" \ 112 | --label "org.opencontainers.image.revision=$(GIT_COMMIT)" \ 113 | --label "org.opencontainers.image.title=Instance Manager" \ 114 | --label "org.opencontainers.image.description=A Kubernetes controller for creating and managing worker node instance groups across multiple providers" \ 115 | --label "org.opencontainers.image.licenses=Apache-2.0" \ 116 | --label "org.opencontainers.image.source=https://github.com/keikoproj/instance-manager" \ 117 | --label "org.opencontainers.image.url=https://github.com/keikoproj/instance-manager/blob/master/README.md" \ 118 | --label "org.opencontainers.image.vendor=keikoproj" \ 119 | --label "org.opencontainers.image.authors=Keikoproj Contributors" 120 | 121 | # Push the docker image 122 | .PHONY: docker-push 123 | docker-push: 124 | docker push ${IMG} 125 | 126 | LOCALBIN = $(shell pwd)/bin 127 | $(LOCALBIN): 128 | mkdir -p $(LOCALBIN) 129 | 130 | # Update controller-gen installation to better support ARM architectures 131 | CONTROLLER_GEN = $(shell pwd)/bin/controller-gen 132 | .PHONY: controller-gen 133 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. 134 | $(CONTROLLER_GEN): $(LOCALBIN) 135 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_GEN_VERSION) 136 | 137 | GOLANGCI_LINT_VERSION := v2.1.1 138 | GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint 139 | .PHONY: golangci-lint 140 | $(GOLANGCI_LINT): $(LOCALBIN) 141 | GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) 142 | 143 | .PHONY: check-go 144 | check-go: 145 | ifeq ($(GO_VERSION_CHECK),0) 146 | $(error go 1.20 or higher is required) 147 | endif 148 | 149 | .PHONY: lint 150 | lint: check-go $(GOLANGCI_LINT) 151 | @echo "Running golangci-lint" 152 | $(GOLANGCI_LINT) run ./... 153 | 154 | .PHONY: clean 155 | clean: 156 | @rm -rf ./bin 157 | -------------------------------------------------------------------------------- /client/clientset/versioned/typed/instancemgr/v1alpha1/fake/fake_instancegroup.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | // Code generated by client-gen. DO NOT EDIT. 16 | 17 | package fake 18 | 19 | import ( 20 | "context" 21 | 22 | v1alpha1 "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | labels "k8s.io/apimachinery/pkg/labels" 25 | types "k8s.io/apimachinery/pkg/types" 26 | watch "k8s.io/apimachinery/pkg/watch" 27 | testing "k8s.io/client-go/testing" 28 | ) 29 | 30 | // FakeInstanceGroups implements InstanceGroupInterface 31 | type FakeInstanceGroups struct { 32 | Fake *FakeInstancemgrV1alpha1 33 | ns string 34 | } 35 | 36 | var instancegroupsResource = v1alpha1.SchemeGroupVersion.WithResource("instancegroups") 37 | 38 | var instancegroupsKind = v1alpha1.SchemeGroupVersion.WithKind("InstanceGroup") 39 | 40 | // Get takes name of the instanceGroup, and returns the corresponding instanceGroup object, and an error if there is any. 41 | func (c *FakeInstanceGroups) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.InstanceGroup, err error) { 42 | obj, err := c.Fake. 43 | Invokes(testing.NewGetAction(instancegroupsResource, c.ns, name), &v1alpha1.InstanceGroup{}) 44 | 45 | if obj == nil { 46 | return nil, err 47 | } 48 | return obj.(*v1alpha1.InstanceGroup), err 49 | } 50 | 51 | // List takes label and field selectors, and returns the list of InstanceGroups that match those selectors. 52 | func (c *FakeInstanceGroups) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.InstanceGroupList, err error) { 53 | obj, err := c.Fake. 54 | Invokes(testing.NewListAction(instancegroupsResource, instancegroupsKind, c.ns, opts), &v1alpha1.InstanceGroupList{}) 55 | 56 | if obj == nil { 57 | return nil, err 58 | } 59 | 60 | label, _, _ := testing.ExtractFromListOptions(opts) 61 | if label == nil { 62 | label = labels.Everything() 63 | } 64 | list := &v1alpha1.InstanceGroupList{ListMeta: obj.(*v1alpha1.InstanceGroupList).ListMeta} 65 | for _, item := range obj.(*v1alpha1.InstanceGroupList).Items { 66 | if label.Matches(labels.Set(item.Labels)) { 67 | list.Items = append(list.Items, item) 68 | } 69 | } 70 | return list, err 71 | } 72 | 73 | // Watch returns a watch.Interface that watches the requested instanceGroups. 74 | func (c *FakeInstanceGroups) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 75 | return c.Fake. 76 | InvokesWatch(testing.NewWatchAction(instancegroupsResource, c.ns, opts)) 77 | 78 | } 79 | 80 | // Create takes the representation of a instanceGroup and creates it. Returns the server's representation of the instanceGroup, and an error, if there is any. 81 | func (c *FakeInstanceGroups) Create(ctx context.Context, instanceGroup *v1alpha1.InstanceGroup, opts v1.CreateOptions) (result *v1alpha1.InstanceGroup, err error) { 82 | obj, err := c.Fake. 83 | Invokes(testing.NewCreateAction(instancegroupsResource, c.ns, instanceGroup), &v1alpha1.InstanceGroup{}) 84 | 85 | if obj == nil { 86 | return nil, err 87 | } 88 | return obj.(*v1alpha1.InstanceGroup), err 89 | } 90 | 91 | // Update takes the representation of a instanceGroup and updates it. Returns the server's representation of the instanceGroup, and an error, if there is any. 92 | func (c *FakeInstanceGroups) Update(ctx context.Context, instanceGroup *v1alpha1.InstanceGroup, opts v1.UpdateOptions) (result *v1alpha1.InstanceGroup, err error) { 93 | obj, err := c.Fake. 94 | Invokes(testing.NewUpdateAction(instancegroupsResource, c.ns, instanceGroup), &v1alpha1.InstanceGroup{}) 95 | 96 | if obj == nil { 97 | return nil, err 98 | } 99 | return obj.(*v1alpha1.InstanceGroup), err 100 | } 101 | 102 | // UpdateStatus was generated because the type contains a Status member. 103 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 104 | func (c *FakeInstanceGroups) UpdateStatus(ctx context.Context, instanceGroup *v1alpha1.InstanceGroup, opts v1.UpdateOptions) (*v1alpha1.InstanceGroup, error) { 105 | obj, err := c.Fake. 106 | Invokes(testing.NewUpdateSubresourceAction(instancegroupsResource, "status", c.ns, instanceGroup), &v1alpha1.InstanceGroup{}) 107 | 108 | if obj == nil { 109 | return nil, err 110 | } 111 | return obj.(*v1alpha1.InstanceGroup), err 112 | } 113 | 114 | // Delete takes name of the instanceGroup and deletes it. Returns an error if one occurs. 115 | func (c *FakeInstanceGroups) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 116 | _, err := c.Fake. 117 | Invokes(testing.NewDeleteActionWithOptions(instancegroupsResource, c.ns, name, opts), &v1alpha1.InstanceGroup{}) 118 | 119 | return err 120 | } 121 | 122 | // DeleteCollection deletes a collection of objects. 123 | func (c *FakeInstanceGroups) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 124 | action := testing.NewDeleteCollectionAction(instancegroupsResource, c.ns, listOpts) 125 | 126 | _, err := c.Fake.Invokes(action, &v1alpha1.InstanceGroupList{}) 127 | return err 128 | } 129 | 130 | // Patch applies the patch and returns the patched instanceGroup. 131 | func (c *FakeInstanceGroups) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.InstanceGroup, err error) { 132 | obj, err := c.Fake. 133 | Invokes(testing.NewPatchSubresourceAction(instancegroupsResource, c.ns, name, pt, data, subresources...), &v1alpha1.InstanceGroup{}) 134 | 135 | if obj == nil { 136 | return nil, err 137 | } 138 | return obj.(*v1alpha1.InstanceGroup), err 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # instance-manager 2 | 3 | [![Build Status][BuildStatusImg]][BuildMasterUrl] 4 | [![Image Push][ImagePushImg]][ImagePushUrl] 5 | [![Codecov][CodecovImg]][CodecovUrl] 6 | [![Go Report Card][GoReportImg]][GoReportUrl] 7 | [![slack][SlackImg]][SlackUrl] 8 | [![Release][ReleaseImg]][ReleaseUrl] 9 | > Create and manage instance groups with Kubernetes. 10 | 11 | **instance-manager** simplifies the creation of worker nodes from within a Kubernetes cluster and creates `InstanceGroup` objects in your cluster. Additionally, **instance-manager** will provision the actual machines and bootstrap them to the cluster. 12 | 13 | ![instance-manager](hack/instance-manager.png) 14 | 15 | - [instance-manager](#instance-manager) 16 | - [Installation](#installation) 17 | - [Usage example](#usage-example) 18 | - [Currently supported provisioners](#currently-supported-provisioners) 19 | - [Submit and Verify](#submit-and-verify) 20 | - [Alpha-2 Version](#alpha-2-version) 21 | - [Contributing](#contributing) 22 | - [Developer Guide](#developer-guide) 23 | 24 | Worker nodes in Kubernetes clusters work best if provisioned and managed using a logical grouping. Kops introduced the term “InstanceGroup” for this logical grouping. In AWS, an InstanceGroup maps to an AutoScalingGroup. 25 | 26 | Given a particular cluster, there should be a way to create, read, upgrade and delete worker nodes from within the cluster itself. This enables use-cases where worker nodes can be created in response to Kubernetes events, InstanceGroups can be automatically assigned to namespaces for multi-tenancy, etc. 27 | 28 | instance-manager provides this Kubernetes native mechanism for CRUD operations on worker nodes. 29 | 30 | ## Installation 31 | 32 | You must first have atleast one instance group that was manually created, in order to host the instance-manager pod. 33 | 34 | _For installation instructions and more examples of usage, please refer to the [Installation Reference Walkthrough][install]._ 35 | 36 | ## Usage example 37 | 38 | ![Demo](./docs/demo.gif) 39 | 40 | ```bash 41 | $ kubectl create -f instance_group.yaml 42 | instancegroup.instancemgr.keikoproj.io/hello-world created 43 | 44 | $ kubectl get instancegroups 45 | NAMESPACE NAME STATE MIN MAX GROUP NAME PROVISIONER STRATEGY LIFECYCLE AGE 46 | instance-manager hello-world ReconcileModifying 3 6 hello-world eks crd normal 1m 47 | ``` 48 | 49 | some time later, once the scaling groups are created 50 | 51 | ```bash 52 | $ kubectl get instancegroups 53 | NAMESPACE NAME STATE MIN MAX GROUP NAME PROVISIONER STRATEGY LIFECYCLE AGE 54 | instance-manager hello-world Ready 3 6 hello-world eks crd normal 7m 55 | ``` 56 | 57 | At this point the new nodes should be joined as well 58 | 59 | ```bash 60 | $ kubectl get nodes 61 | NAME STATUS ROLES AGE VERSION 62 | ip-10-10-10-10.us-west-2.compute.internal Ready system 2h v1.14.6-eks-5047ed 63 | ip-10-10-10-20.us-west-2.compute.internal Ready hello-world 32s v1.14.6-eks-5047ed 64 | ip-10-10-10-30.us-west-2.compute.internal Ready hello-world 32s v1.14.6-eks-5047ed 65 | ip-10-10-10-40.us-west-2.compute.internal Ready hello-world 32s v1.14.6-eks-5047ed 66 | ``` 67 | 68 | ### Provisioners 69 | 70 | | Provisioner | Description | Documentation | API Reference | Maturity | 71 | | :---------- | :---------- | :----------| :----------| :----------| 72 | | eks | provision nodes on EKS | [Documentation](./docs/examples/EKS.md) | [API Reference](./docs/EKS.md#api-reference)| Production 73 | | eks-managed | provision managed node groups on EKS| [Documentation](./docs/examples/EKS-managed.md) | | Experimental 74 | | eks-fargate | provision a cluster to run pods on EKS Fargate| [Documentation](./docs/examples/EKS-fargate.md) | | Experimental 75 | 76 | To create an instance group, submit an InstanceGroup custom resource in your cluster, and the controller will provision and bootstrap it to your cluster, and allow you to modify it from within the cluster. 77 | 78 | ### Alpha-2 Version 79 | 80 | Please consider that this project is in alpha stages and breaking API changes may happen, we will do our best to not break backwards compatiblity without a deprecation period going further. 81 | 82 | The previous eks-cf provisioner have been discontinued in favor of the Alpha-2 eks provisioner, which does not use cloudformation as a mechanism to provision the required resources. 83 | 84 | In order to migrate instance-groups from versions <0.5.0, delete all instance groups, update the custom resource definition RBAC, and controller IAM role, and deploy new instance-groups with the new provisioner. 85 | 86 | ## Contributing 87 | 88 | Please see [CONTRIBUTING.md](.github/CONTRIBUTING.md). 89 | 90 | ## Developer Guide 91 | 92 | Please see [DEVELOPER.md](.github/DEVELOPER.md). 93 | 94 | 95 | [install]: https://github.com/keikoproj/instance-manager/blob/master/docs/INSTALL.md 96 | [SlackUrl]: https://keikoproj.slack.com/ 97 | [SlackImg]: https://img.shields.io/badge/slack-join%20the%20conversation-ff69b4.svg 98 | 99 | [BuildStatusImg]: https://github.com/keikoproj/instance-manager/actions/workflows/unit-test.yml/badge.svg 100 | [BuildMasterUrl]: https://github.com/keikoproj/instance-manager/actions/workflows/unit-test.yml 101 | 102 | [ImagePushImg]: https://github.com/keikoproj/instance-manager/actions/workflows/image-push.yml/badge.svg 103 | [ImagePushUrl]: https://github.com/keikoproj/instance-manager/actions/workflows/image-push.yml 104 | 105 | [CodecovImg]: https://codecov.io/gh/keikoproj/instance-manager/branch/master/graph/badge.svg?token=IJbjmSBliL 106 | [CodecovUrl]: https://codecov.io/gh/keikoproj/instance-manager 107 | 108 | [GoReportImg]: https://goreportcard.com/badge/github.com/keikoproj/instance-manager 109 | [GoReportUrl]: https://goreportcard.com/report/github.com/keikoproj/instance-manager 110 | 111 | [ReleaseImg]: https://img.shields.io/github/release/keikoproj/instance-manager.svg 112 | [ReleaseUrl]: https://github.com/keikoproj/instance-manager/releases/latest 113 | -------------------------------------------------------------------------------- /controllers/providers/kubernetes/utils_test.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/keikoproj/instance-manager/controllers/common" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/types" 10 | ) 11 | 12 | func TestHasAnnotation(t *testing.T) { 13 | annotations := map[string]string{ 14 | "foo": "bar", 15 | "baz": "", 16 | } 17 | tests := []struct { 18 | name string 19 | annotations map[string]string 20 | key string 21 | expected bool 22 | }{ 23 | { 24 | name: "present with value", 25 | annotations: annotations, 26 | key: "foo", 27 | expected: true, 28 | }, 29 | { 30 | name: "present with no value", 31 | annotations: annotations, 32 | key: "baz", 33 | expected: true, 34 | }, 35 | { 36 | name: "absent", 37 | annotations: annotations, 38 | key: "missing", 39 | expected: false, 40 | }, 41 | } 42 | 43 | for _, tc := range tests { 44 | result := HasAnnotation(tc.annotations, tc.key) 45 | if result != tc.expected { 46 | t.Fail() 47 | } 48 | } 49 | } 50 | 51 | func TestHasAnnotationWithValue(t *testing.T) { 52 | annotations := map[string]string{ 53 | "foo": "bar", 54 | "baz": "", 55 | } 56 | tests := []struct { 57 | name string 58 | annotations map[string]string 59 | key string 60 | value string 61 | expected bool 62 | }{ 63 | { 64 | name: "present with value expecting value", 65 | annotations: annotations, 66 | key: "foo", 67 | value: "bar", 68 | expected: true, 69 | }, 70 | { 71 | name: "present with value expecting no value", 72 | annotations: annotations, 73 | key: "foo", 74 | value: "", 75 | expected: false, 76 | }, 77 | { 78 | name: "present with no value expecting no value", 79 | annotations: annotations, 80 | key: "baz", 81 | value: "", 82 | expected: true, 83 | }, 84 | { 85 | name: "present with no value expecting value", 86 | annotations: annotations, 87 | key: "baz", 88 | value: "boop", 89 | expected: false, 90 | }, 91 | { 92 | name: "absent", 93 | annotations: annotations, 94 | key: "missing", 95 | value: "", 96 | expected: false, 97 | }, 98 | } 99 | 100 | for _, tc := range tests { 101 | result := HasAnnotationWithValue(tc.annotations, tc.key, tc.value) 102 | if result != tc.expected { 103 | t.Fatalf("Unexpected result %v. expected %v from %s", result, tc.expected, tc.name) 104 | } 105 | } 106 | } 107 | 108 | // Test IsStorageError 109 | func TestIsStorageError(t *testing.T) { 110 | tests := []struct { 111 | name string 112 | err error 113 | expected bool 114 | }{ 115 | { 116 | name: "storage error", 117 | err: errors.New("StorageError: invalid object"), 118 | expected: true, 119 | }, 120 | { 121 | name: "storage error different case", 122 | err: errors.New("sToRaGeErRor: invalid object"), 123 | expected: true, 124 | }, 125 | { 126 | name: "not a storage error", 127 | err: errors.New("some other error"), 128 | expected: false, 129 | }, 130 | } 131 | 132 | for _, tc := range tests { 133 | t.Run(tc.name, func(t *testing.T) { 134 | result := IsStorageError(tc.err) 135 | if result != tc.expected { 136 | t.Errorf("IsStorageError(%v) = %v, expected %v", tc.err, result, tc.expected) 137 | } 138 | }) 139 | } 140 | } 141 | 142 | // Test IsPathValue 143 | func TestIsPathValue(t *testing.T) { 144 | // Create a test unstructured resource 145 | resource := unstructured.Unstructured{ 146 | Object: map[string]interface{}{ 147 | "spec": map[string]interface{}{ 148 | "foo": "bar", 149 | }, 150 | }, 151 | } 152 | 153 | tests := []struct { 154 | name string 155 | resource unstructured.Unstructured 156 | path string 157 | value string 158 | expected bool 159 | }{ 160 | { 161 | name: "path exists with matching value", 162 | resource: resource, 163 | path: "spec.foo", 164 | value: "bar", 165 | expected: true, 166 | }, 167 | { 168 | name: "path exists with matching value different case", 169 | resource: resource, 170 | path: "spec.foo", 171 | value: "BAR", 172 | expected: true, 173 | }, 174 | { 175 | name: "path exists with non-matching value", 176 | resource: resource, 177 | path: "spec.foo", 178 | value: "baz", 179 | expected: false, 180 | }, 181 | { 182 | name: "path does not exist", 183 | resource: resource, 184 | path: "spec.missing", 185 | value: "any", 186 | expected: false, 187 | }, 188 | } 189 | 190 | for _, tc := range tests { 191 | t.Run(tc.name, func(t *testing.T) { 192 | result := IsPathValue(tc.resource, tc.path, tc.value) 193 | if result != tc.expected { 194 | t.Errorf("IsPathValue() = %v, expected %v", result, tc.expected) 195 | } 196 | }) 197 | } 198 | } 199 | 200 | // Test ObjectDigest 201 | func TestObjectDigest(t *testing.T) { 202 | testObj := map[string]string{"foo": "bar"} 203 | 204 | tests := []struct { 205 | name string 206 | obj interface{} 207 | expected string 208 | }{ 209 | { 210 | name: "nil object", 211 | obj: nil, 212 | expected: "N/A", 213 | }, 214 | { 215 | name: "non-nil object", 216 | obj: testObj, 217 | expected: common.StringMD5("map[foo:bar]"), 218 | }, 219 | } 220 | 221 | for _, tc := range tests { 222 | t.Run(tc.name, func(t *testing.T) { 223 | result := ObjectDigest(tc.obj) 224 | if tc.expected != result { 225 | t.Errorf("ObjectDigest(%v) = %v, expected %v", tc.obj, result, tc.expected) 226 | } 227 | }) 228 | } 229 | } 230 | 231 | // Test NewStatusPatch 232 | func TestNewStatusPatch(t *testing.T) { 233 | patch := NewStatusPatch() 234 | 235 | if patch == nil { 236 | t.Error("NewStatusPatch() returned nil") 237 | return 238 | } 239 | 240 | // Basic validation that the patch was created with empty spec 241 | if patch.from.Spec.Provisioner != "" { 242 | t.Errorf("Expected empty provisioner in statusPatch, got %s", patch.from.Spec.Provisioner) 243 | } 244 | 245 | // Verify the patch type is MergePatchType 246 | patchType := patch.Type() 247 | if patchType != types.MergePatchType { 248 | t.Errorf("Expected patch type types.MergePatchType, got %v", patchType) 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /controllers/provisioners/eks/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package eks 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/keikoproj/instance-manager/controllers/provisioners/eks/scaling" 22 | 23 | "github.com/keikoproj/instance-manager/api/instancemgr/v1alpha1" 24 | kubeprovider "github.com/keikoproj/instance-manager/controllers/providers/kubernetes" 25 | "github.com/pkg/errors" 26 | 27 | "github.com/aws/aws-sdk-go/aws" 28 | "github.com/aws/aws-sdk-go/service/autoscaling" 29 | "github.com/keikoproj/instance-manager/controllers/common" 30 | ) 31 | 32 | func (ctx *EksInstanceGroupContext) Create() error { 33 | var ( 34 | instanceGroup = ctx.GetInstanceGroup() 35 | state = ctx.GetDiscoveredState() 36 | scalingConfig = state.GetScalingConfiguration() 37 | configuration = instanceGroup.GetEKSConfiguration() 38 | args = ctx.GetBootstrapArgs() 39 | kubeletArgs = ctx.GetKubeletExtraArgs() 40 | userDataPayload = ctx.GetUserDataStages() 41 | clusterName = configuration.GetClusterName() 42 | mounts = ctx.GetMountOpts() 43 | userData = ctx.GetBasicUserData(clusterName, args, kubeletArgs, userDataPayload, mounts) 44 | sgs = ctx.ResolveSecurityGroups() 45 | spotPrice = configuration.GetSpotPrice() 46 | placement = configuration.GetPlacement() 47 | metadataOptions = configuration.GetMetadataOptions() 48 | ) 49 | ctx.SetState(v1alpha1.ReconcileModifying) 50 | 51 | // no need to create a role if one is already provided 52 | err := ctx.CreateManagedRole() 53 | if err != nil { 54 | return errors.Wrap(err, "failed to create scaling group role") 55 | } 56 | instanceProfile := state.GetInstanceProfile() 57 | 58 | var configName = scalingConfig.Name() 59 | 60 | if common.StringEmpty(configName) { 61 | configName = fmt.Sprintf("%v-%v", ctx.ResourcePrefix, common.GetTimeString()) 62 | } 63 | 64 | config := &scaling.CreateConfigurationInput{ 65 | Name: configName, 66 | IamInstanceProfileArn: aws.StringValue(instanceProfile.Arn), 67 | ImageId: configuration.Image, 68 | InstanceType: configuration.InstanceType, 69 | KeyName: configuration.KeyPairName, 70 | SecurityGroups: sgs, 71 | Volumes: configuration.Volumes, 72 | UserData: userData, 73 | SpotPrice: spotPrice, 74 | LicenseSpecifications: configuration.LicenseSpecifications, 75 | Placement: placement, 76 | MetadataOptions: metadataOptions, 77 | } 78 | 79 | if err := scalingConfig.Create(config); err != nil { 80 | return errors.Wrap(err, "failed to create scaling configuration") 81 | } 82 | 83 | // create scaling group 84 | err = ctx.CreateScalingGroup(configName) 85 | if err != nil { 86 | return errors.Wrap(err, "failed to create scaling group") 87 | } 88 | 89 | ctx.SetState(v1alpha1.ReconcileModified) 90 | return nil 91 | } 92 | 93 | func (ctx *EksInstanceGroupContext) CreateScalingGroup(name string) error { 94 | var ( 95 | instanceGroup = ctx.GetInstanceGroup() 96 | status = instanceGroup.GetStatus() 97 | spec = instanceGroup.GetEKSSpec() 98 | configuration = instanceGroup.GetEKSConfiguration() 99 | state = ctx.GetDiscoveredState() 100 | asgName = ctx.ResourcePrefix 101 | tags = ctx.GetAddedTags(asgName) 102 | ) 103 | 104 | if state.HasScalingGroup() { 105 | return nil 106 | } 107 | 108 | input := &autoscaling.CreateAutoScalingGroupInput{ 109 | AutoScalingGroupName: aws.String(asgName), 110 | DesiredCapacity: aws.Int64(spec.GetMinSize()), 111 | MinSize: aws.Int64(spec.GetMinSize()), 112 | MaxSize: aws.Int64(spec.GetMaxSize()), 113 | VPCZoneIdentifier: aws.String(common.ConcatenateList(ctx.ResolveSubnets(), ",")), 114 | Tags: tags, 115 | } 116 | 117 | if spec.IsLaunchConfiguration() { 118 | input.LaunchConfigurationName = aws.String(name) 119 | status.SetActiveLaunchConfigurationName(name) 120 | } 121 | 122 | if spec.IsLaunchTemplate() { 123 | if policy := configuration.GetMixedInstancesPolicy(); policy != nil { 124 | input.MixedInstancesPolicy = ctx.GetDesiredMixedInstancesPolicy(name) 125 | } else { 126 | input.LaunchTemplate = &autoscaling.LaunchTemplateSpecification{ 127 | LaunchTemplateName: aws.String(name), 128 | Version: aws.String("$Latest"), 129 | } 130 | } 131 | status.SetActiveLaunchTemplateName(name) 132 | } 133 | 134 | err := ctx.AwsWorker.CreateScalingGroup(input) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | ctx.Log.Info("created scaling group", "instancegroup", instanceGroup.NamespacedName(), "scalinggroup", asgName) 140 | 141 | if err := ctx.UpdateScalingProcesses(asgName); err != nil { 142 | return err 143 | } 144 | 145 | if err := ctx.UpdateMetricsCollection(asgName); err != nil { 146 | return err 147 | } 148 | 149 | if err := ctx.UpdateLifecycleHooks(asgName); err != nil { 150 | return err 151 | } 152 | 153 | state.Publisher.Publish(kubeprovider.InstanceGroupCreatedEvent, "instancegroup", instanceGroup.NamespacedName(), "scalinggroup", asgName) 154 | return nil 155 | } 156 | 157 | func (ctx *EksInstanceGroupContext) CreateManagedRole() error { 158 | var ( 159 | instanceGroup = ctx.GetInstanceGroup() 160 | state = ctx.GetDiscoveredState() 161 | configuration = instanceGroup.GetEKSConfiguration() 162 | roleName = ctx.ResourcePrefix 163 | ) 164 | 165 | if configuration.HasExistingRole() { 166 | // avoid updating if using an existing role 167 | return nil 168 | } 169 | 170 | if len(roleName) > 63 { 171 | // use a hash of the actual name in case we exceed the max length 172 | roleName = common.StringMD5(roleName) 173 | } 174 | 175 | role, profile, err := ctx.AwsWorker.CreateScalingGroupRole(roleName) 176 | if err != nil { 177 | return errors.Wrap(err, "failed to create scaling group role") 178 | } 179 | 180 | err = ctx.UpdateManagedPolicies(roleName) 181 | if err != nil { 182 | return errors.Wrap(err, "failed to update managed policies") 183 | } 184 | 185 | ctx.Log.Info("reconciled managed role", "instancegroup", instanceGroup.NamespacedName(), "iamrole", roleName) 186 | 187 | state.SetRole(role) 188 | state.SetInstanceProfile(profile) 189 | 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /controllers/providers/aws/constructors.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package aws 17 | 18 | import ( 19 | "github.com/aws/aws-sdk-go/aws" 20 | "github.com/aws/aws-sdk-go/service/autoscaling" 21 | "github.com/aws/aws-sdk-go/service/ec2" 22 | "github.com/aws/aws-sdk-go/service/eks" 23 | "github.com/keikoproj/instance-manager/controllers/common" 24 | ) 25 | 26 | func (w *AwsWorker) GetAutoScalingBasicBlockDevice(name, volType, snapshot string, volSize, iops int64, throughput int64, delete, encrypt *bool) *autoscaling.BlockDeviceMapping { 27 | device := &autoscaling.BlockDeviceMapping{ 28 | DeviceName: aws.String(name), 29 | Ebs: &autoscaling.Ebs{ 30 | VolumeType: aws.String(volType), 31 | }, 32 | } 33 | if delete != nil { 34 | device.Ebs.DeleteOnTermination = delete 35 | } else { 36 | device.Ebs.DeleteOnTermination = aws.Bool(true) 37 | } 38 | if encrypt != nil { 39 | device.Ebs.Encrypted = encrypt 40 | } 41 | if iops != 0 && common.ContainsEqualFold(AllowedVolumeTypesWithProvisionedIOPS, volType) { 42 | device.Ebs.Iops = aws.Int64(iops) 43 | } 44 | if throughput != 0 && common.ContainsEqualFold(AllowedVolumeTypesWithProvisionedThroughput, volType) { 45 | device.Ebs.Throughput = aws.Int64(throughput) 46 | } 47 | if volSize != 0 { 48 | device.Ebs.VolumeSize = aws.Int64(volSize) 49 | } 50 | if !common.StringEmpty(snapshot) { 51 | device.Ebs.SnapshotId = aws.String(snapshot) 52 | } 53 | return device 54 | } 55 | 56 | func (w *AwsWorker) GetLaunchTemplateBlockDeviceRequest(name, volType, snapshot string, volSize, iops int64, throughput int64, delete, encrypt *bool) *ec2.LaunchTemplateBlockDeviceMappingRequest { 57 | device := &ec2.LaunchTemplateBlockDeviceMappingRequest{ 58 | DeviceName: aws.String(name), 59 | Ebs: &ec2.LaunchTemplateEbsBlockDeviceRequest{ 60 | VolumeType: aws.String(volType), 61 | }, 62 | } 63 | if delete != nil { 64 | device.Ebs.DeleteOnTermination = delete 65 | } else { 66 | device.Ebs.DeleteOnTermination = aws.Bool(true) 67 | } 68 | if encrypt != nil { 69 | device.Ebs.Encrypted = encrypt 70 | } 71 | if iops != 0 && common.ContainsEqualFold(AllowedVolumeTypesWithProvisionedIOPS, volType) { 72 | device.Ebs.Iops = aws.Int64(iops) 73 | } 74 | if throughput != 0 && common.ContainsEqualFold(AllowedVolumeTypesWithProvisionedThroughput, volType) { 75 | device.Ebs.Throughput = aws.Int64(throughput) 76 | } 77 | if volSize != 0 { 78 | device.Ebs.VolumeSize = aws.Int64(volSize) 79 | } 80 | if !common.StringEmpty(snapshot) { 81 | device.Ebs.SnapshotId = aws.String(snapshot) 82 | } 83 | 84 | return device 85 | } 86 | 87 | func (w *AwsWorker) GetLaunchTemplateBlockDevice(name, volType, snapshot string, volSize, iops int64, throughput int64, delete, encrypt *bool) *ec2.LaunchTemplateBlockDeviceMapping { 88 | device := &ec2.LaunchTemplateBlockDeviceMapping{ 89 | DeviceName: aws.String(name), 90 | Ebs: &ec2.LaunchTemplateEbsBlockDevice{ 91 | VolumeType: aws.String(volType), 92 | }, 93 | } 94 | if delete != nil { 95 | device.Ebs.DeleteOnTermination = delete 96 | } else { 97 | device.Ebs.DeleteOnTermination = aws.Bool(true) 98 | } 99 | if encrypt != nil { 100 | device.Ebs.Encrypted = encrypt 101 | } 102 | if iops != 0 && common.ContainsEqualFold(AllowedVolumeTypesWithProvisionedIOPS, volType) { 103 | device.Ebs.Iops = aws.Int64(iops) 104 | } 105 | if throughput != 0 && common.ContainsEqualFold(AllowedVolumeTypesWithProvisionedThroughput, volType) { 106 | device.Ebs.Throughput = aws.Int64(throughput) 107 | } 108 | if volSize != 0 { 109 | device.Ebs.VolumeSize = aws.Int64(volSize) 110 | } 111 | if !common.StringEmpty(snapshot) { 112 | device.Ebs.SnapshotId = aws.String(snapshot) 113 | } 114 | 115 | return device 116 | } 117 | 118 | func (w *AwsWorker) LaunchTemplatePlacementRequest(availabilityZone, hostResourceGroupArn, tenancy string) *ec2.LaunchTemplatePlacementRequest { 119 | placement := &ec2.LaunchTemplatePlacementRequest{} 120 | 121 | if !common.StringEmpty(availabilityZone) { 122 | placement.AvailabilityZone = aws.String(availabilityZone) 123 | } 124 | 125 | if !common.StringEmpty(hostResourceGroupArn) { 126 | placement.HostResourceGroupArn = aws.String(hostResourceGroupArn) 127 | } 128 | 129 | if !common.StringEmpty(tenancy) { 130 | placement.Tenancy = aws.String(tenancy) 131 | } 132 | 133 | return placement 134 | } 135 | 136 | func (w *AwsWorker) LaunchTemplatePlacement(availabilityZone, hostResourceGroupArn, tenancy string) *ec2.LaunchTemplatePlacement { 137 | placement := &ec2.LaunchTemplatePlacement{} 138 | 139 | if !common.StringEmpty(availabilityZone) { 140 | placement.AvailabilityZone = aws.String(availabilityZone) 141 | } 142 | 143 | if !common.StringEmpty(hostResourceGroupArn) { 144 | placement.HostResourceGroupArn = aws.String(hostResourceGroupArn) 145 | } 146 | 147 | if !common.StringEmpty(tenancy) { 148 | placement.Tenancy = aws.String(tenancy) 149 | } 150 | 151 | return placement 152 | } 153 | 154 | func (w *AwsWorker) LaunchTemplateLicenseConfigurationRequest(input []string) []*ec2.LaunchTemplateLicenseConfigurationRequest { 155 | var licenses []*ec2.LaunchTemplateLicenseConfigurationRequest 156 | for _, v := range input { 157 | licenses = append(licenses, &ec2.LaunchTemplateLicenseConfigurationRequest{ 158 | LicenseConfigurationArn: aws.String(v), 159 | }) 160 | } 161 | return licenses 162 | } 163 | 164 | func (w *AwsWorker) LaunchTemplateLicenseConfiguration(input []string) []*ec2.LaunchTemplateLicenseConfiguration { 165 | var licenses []*ec2.LaunchTemplateLicenseConfiguration 166 | for _, v := range input { 167 | licenses = append(licenses, &ec2.LaunchTemplateLicenseConfiguration{ 168 | LicenseConfigurationArn: aws.String(v), 169 | }) 170 | } 171 | return licenses 172 | } 173 | 174 | func (w *AwsWorker) NewTag(key, val, resource string) *autoscaling.Tag { 175 | return &autoscaling.Tag{ 176 | Key: aws.String(key), 177 | Value: aws.String(val), 178 | PropagateAtLaunch: aws.Bool(true), 179 | ResourceId: aws.String(resource), 180 | ResourceType: aws.String("auto-scaling-group"), 181 | } 182 | } 183 | 184 | func (w *AwsWorker) GetLabelsUpdatePayload(existing, new map[string]string) (*eks.UpdateLabelsPayload, bool) { 185 | 186 | var ( 187 | removeLabels = make([]string, 0) 188 | addUpdateLabels = make(map[string]string) 189 | ) 190 | 191 | payload := &eks.UpdateLabelsPayload{} 192 | for k, v := range new { 193 | // handle new labels 194 | if _, ok := existing[k]; !ok { 195 | addUpdateLabels[k] = v 196 | } 197 | 198 | // handle label value updates 199 | if val, ok := existing[k]; ok && val != v { 200 | addUpdateLabels[k] = v 201 | } 202 | } 203 | 204 | for k := range existing { 205 | // handle removals 206 | if _, ok := new[k]; !ok { 207 | removeLabels = append(removeLabels, k) 208 | } 209 | } 210 | 211 | if len(addUpdateLabels) > 0 { 212 | payload.AddOrUpdateLabels = aws.StringMap(addUpdateLabels) 213 | } 214 | 215 | if len(removeLabels) > 0 { 216 | payload.RemoveLabels = aws.StringSlice(removeLabels) 217 | } 218 | 219 | if payload.RemoveLabels == nil && payload.AddOrUpdateLabels == nil { 220 | return payload, false 221 | } 222 | 223 | return payload, true 224 | } 225 | --------------------------------------------------------------------------------