├── .dockerignore ├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── conditions.go │ ├── groupversion_info.go │ ├── irsa_types.go │ ├── irsa_types_test.go │ ├── irsasetup_types.go │ └── zz_generated.deepcopy.go ├── charts └── irsa-manager │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── README.md.gotmpl │ ├── crds │ ├── irsa-crd.yaml │ └── irsasetup-crd.yaml │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── leader-election-rbac.yaml │ ├── manager-rbac.yaml │ ├── metrics-reader-rbac.yaml │ ├── metrics-service.yaml │ ├── proxy-rbac.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── cmd └── main.go ├── config ├── crd │ ├── bases │ │ ├── irsa-manager.kkb0318.github.io_irsas.yaml │ │ └── irsa-manager.kkb0318.github.io_irsasetups.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── default │ └── kustomization.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── irsa_editor_role.yaml │ ├── irsa_viewer_role.yaml │ ├── irsasetup_editor_role.yaml │ ├── irsasetup_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── release │ └── kustomization.yaml ├── samples │ ├── irsa_v1alpha1_irsa.yaml │ ├── irsa_v1alpha1_irsasetup.yaml │ └── kustomization.yaml └── scorecard │ ├── bases │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── cr.yaml ├── docs ├── IRSA-cr.png ├── IRSASetup-cr.png ├── api.md ├── config.yaml ├── eks-setup.md ├── irsa-manager-overview.png └── selfhosted-setup.md ├── examples ├── eks.yaml ├── irsa.yaml └── selfhosted.yaml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal ├── aws │ ├── aws.go │ ├── aws_role.go │ ├── aws_role_test.go │ └── client.go ├── controller │ ├── irsa_controller.go │ ├── irsa_controller_test.go │ ├── irsasetup_controller.go │ ├── irsasetup_controller_test.go │ └── suite_test.go ├── eks │ └── validation.go ├── handler │ ├── handler.go │ ├── kubernetes.go │ └── status.go ├── issuer │ └── issuer.go ├── kubernetes │ ├── apply.go │ ├── client.go │ ├── create.go │ ├── delete.go │ ├── get.go │ ├── list.go │ ├── owner.go │ ├── status.go │ └── unstructured.go ├── manifests │ ├── secret.go │ └── serviceaccount.go ├── selfhosted │ ├── certificate.go │ ├── jwks.go │ ├── jwks_test.go │ ├── keys.go │ ├── keys_test.go │ ├── oidc.go │ ├── oidc │ │ ├── factory.go │ │ ├── id_provider.go │ │ ├── id_provider_discovery.go │ │ └── id_provider_discovery_contents.go │ ├── selfhosted.go │ ├── testdata │ │ ├── ecdsa.pub │ │ └── rsa.pub │ ├── webhook.go │ └── webhook │ │ ├── base_manifests.go │ │ ├── base_manifests_test.go │ │ ├── certificate.go │ │ ├── certificate_test.go │ │ ├── testdata │ │ ├── clusterrole.yaml │ │ ├── clusterrolebinding.yaml │ │ ├── deployment.yaml │ │ ├── mutatingwebhook.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ │ └── webhook.go └── utils │ ├── diff.go │ └── diff_test.go └── validation ├── job-amd.yaml.template ├── job-arm.yaml.template └── s3-echoer.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - "main" 7 | jobs: 8 | test: 9 | name: Unit, Integration Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version-file: go.mod 16 | - run: make test 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: build-docker-image 2 | 3 | on: 4 | push: 5 | tags: 6 | - "[0-2].[0-9]+.[0-9]+*" 7 | workflow_dispatch: 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | build-and-push-image: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | # For multi platform build. See https://docs.docker.com/build/ci/github-actions/multi-platform/ 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@v5 33 | with: 34 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 35 | tags: | 36 | type=semver,pattern={{version}} 37 | - name: Login to Container Registry 38 | uses: docker/login-action@v3 39 | with: 40 | registry: ${{ env.REGISTRY }} 41 | username: ${{ github.actor }} 42 | password: ${{ secrets.PAT }} 43 | 44 | - name: Build and push Docker image 45 | uses: docker/build-push-action@v5 46 | with: 47 | context: . 48 | push: true 49 | tags: ${{ steps.meta.outputs.tags }} 50 | labels: ${{ steps.meta.outputs.labels }} 51 | platforms: linux/amd64,linux/arm64 52 | chart-release: 53 | runs-on: ubuntu-20.04 54 | needs: build-and-push-image 55 | permissions: 56 | contents: write 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | with: 61 | fetch-depth: 0 62 | - name: Configure Git 63 | run: | 64 | git config user.name "$GITHUB_ACTOR" 65 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 66 | - name: Install Helm 67 | uses: azure/setup-helm@v3 68 | - name: sed 69 | run: | 70 | sed -i "s/CHART_VERSION/${{ github.ref_name }}/" charts/irsa-manager/Chart.yaml 71 | sed -i "s/APP_VERSION/${{ github.ref_name }}/" charts/irsa-manager/Chart.yaml 72 | sed -i "s/APP_VERSION/${{ github.ref_name }}/" charts/irsa-manager/values.yaml 73 | - name: Run chart-releaser 74 | uses: helm/chart-releaser-action@v1.6.0 75 | with: 76 | config: cr.yaml 77 | env: 78 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin/* 9 | Dockerfile.cross 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Kubernetes Generated files - skip generated files, except for vendored files 21 | !vendor/**/zz_generated.* 22 | 23 | # editor and IDE paraphernalia 24 | .idea 25 | .vscode 26 | *.swp 27 | *.swo 28 | *~ 29 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 5m 3 | allow-parallel-runners: true 4 | 5 | issues: 6 | # don't skip warning about doc comments 7 | # don't exclude the default set of lint 8 | exclude-use-default: false 9 | # restore some of the defaults 10 | # (fill in the rest as needed) 11 | exclude-rules: 12 | - path: "api/*" 13 | linters: 14 | - lll 15 | - path: "internal/*" 16 | linters: 17 | - dupl 18 | - lll 19 | linters: 20 | disable-all: true 21 | enable: 22 | - dupl 23 | - errcheck 24 | - exportloopref 25 | - goconst 26 | - gocyclo 27 | - gofmt 28 | - goimports 29 | - gosimple 30 | - govet 31 | - ineffassign 32 | - lll 33 | - misspell 34 | - nakedret 35 | - prealloc 36 | - staticcheck 37 | - typecheck 38 | - unconvert 39 | - unparam 40 | - unused 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.22 AS builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY internal/ internal/ 18 | 19 | # Build 20 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 25 | 26 | # Use distroless as minimal base image to package the manager binary 27 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 28 | FROM gcr.io/distroless/static:nonroot 29 | WORKDIR / 30 | COPY --from=builder /workspace/manager . 31 | USER 65532:65532 32 | 33 | ENTRYPOINT ["/manager"] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 kkb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: kkb0318.github.io 6 | layout: 7 | - go.kubebuilder.io/v4 8 | plugins: 9 | manifests.sdk.operatorframework.io/v2: {} 10 | scorecard.sdk.operatorframework.io/v2: {} 11 | projectName: irsa-manager 12 | repo: github.com/kkb0318/irsa-manager 13 | resources: 14 | - api: 15 | crdVersion: v1 16 | namespaced: true 17 | controller: true 18 | domain: kkb0318.github.io 19 | group: irsa 20 | kind: IRSASetup 21 | path: github.com/kkb0318/irsa-manager/api/v1alpha1 22 | version: v1alpha1 23 | - api: 24 | crdVersion: v1 25 | namespaced: true 26 | controller: true 27 | domain: kkb0318.github.io 28 | group: irsa 29 | kind: IRSA 30 | path: github.com/kkb0318/irsa-manager/api/v1alpha1 31 | version: v1alpha1 32 | version: "3" 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IRSA Manager 2 | 3 | [![GitHub release](https://img.shields.io/github/release/kkb0318/irsa-manager.svg?maxAge=60)](https://github.com/kkb0318/irsa-manager/releases) 4 | [![CI](https://github.com/kkb0318/irsa-manager/actions/workflows/ci.yaml/badge.svg)](https://github.com/kkb0318/irsa-manager/actions/workflows/ci.yaml) 5 | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/irsa-manager)](https://artifacthub.io/packages/search?repo=irsa-manager) 6 | 7 | IRSA Manager allows you to easily set up IAM Roles for Service Accounts (IRSA) on both EKS and non-EKS Kubernetes clusters. 8 | 9 | ![](docs/irsa-manager-overview.png) 10 | 11 | ## Introduction 12 | 13 | IRSA (IAM Roles for Service Accounts) allows Kubernetes service accounts to assume AWS IAM roles. 14 | This is particularly useful for providing Kubernetes workloads with the necessary AWS permissions in a secure manner. 15 | 16 | For detailed guidelines on how irsa-manager works, please refer to the [**blog post**](https://medium.com/@kkb0318/simplify-aws-irsa-for-self-hosted-kubernetes-with-irsa-manager-c2fb2ecf88c5). 17 | 18 | ## Prerequisites 19 | 20 | Before you begin, ensure you have the following: 21 | 22 | - A running Kubernetes cluster. 23 | - Helm installed on your local machine. 24 | - AWS user credentials with appropriate permissions. 25 | 26 | - The permissions should allow irsa-manager to call the necessary AWS APIs. The following outlines the required permissions for self-hosted Kubernetes and EKS environments. 27 | 28 |
29 | for self-hosted 30 | 31 | ```json 32 | { 33 | "Version": "2012-10-17", 34 | "Statement": [ 35 | { 36 | "Effect": "Allow", 37 | "Action": [ 38 | "iam:CreateOpenIDConnectProvider", 39 | "iam:DeleteOpenIDConnectProvider", 40 | "iam:CreateRole", 41 | "iam:UpdateAssumeRolePolicy", 42 | "iam:AttachRolePolicy", 43 | "iam:DeleteRole", 44 | "iam:DetachRolePolicy", 45 | "iam:ListAttachedRolePolicies", 46 | "sts:GetCallerIdentity", 47 | "s3:*" 48 | ], 49 | "Resource": "*" 50 | } 51 | ] 52 | } 53 | ``` 54 | 55 |
56 | 57 |
58 | for EKS 59 | 60 | ```json 61 | { 62 | "Version": "2012-10-17", 63 | "Statement": [ 64 | { 65 | "Effect": "Allow", 66 | "Action": [ 67 | "iam:CreateRole", 68 | "iam:UpdateAssumeRolePolicy", 69 | "iam:AttachRolePolicy", 70 | "iam:DeleteRole", 71 | "iam:DetachRolePolicy", 72 | "iam:ListAttachedRolePolicies", 73 | "sts:GetCallerIdentity" 74 | ], 75 | "Resource": "*" 76 | } 77 | ] 78 | } 79 | ``` 80 | 81 |
82 | 83 | ## Setup 84 | 85 | Follow these steps to set up IRSA on your cluster: 86 | 87 | 1. Set AWS Secret for IRSA Manager 88 | 89 | Create a secret for irsa-manager to access AWS: 90 | 91 | ```console 92 | kubectl create secret generic aws-secret -n irsa-manager-system \ 93 | --from-literal=aws-access-key-id= \ 94 | --from-literal=aws-secret-access-key= \ 95 | --from-literal=aws-session-token= # Optional \ 96 | --from-literal=aws-region= \ 97 | --from-literal=aws-role-arn= # Optional: Set this if you want to switch roles 98 | 99 | ``` 100 | 101 | 2. install helm 102 | 103 | Add the irsa-manager Helm repository and install irsa-manager: 104 | 105 | ```console 106 | helm repo add kkb0318 https://kkb0318.github.io/irsa-manager 107 | helm repo update 108 | helm install irsa-manager kkb0318/irsa-manager -n irsa-manager-system --create-namespace 109 | ``` 110 | 111 | 3. Create an IRSASetup Custom Resource 112 | 113 | If you're using self-hosted Kubernetes, follow this setup: 114 | 115 | [self-hosted setup](./docs/selfhosted-setup.md) 116 | 117 | If you're using EKS, follow this setup: 118 | 119 | [eks setup](./docs/eks-setup.md) 120 | 121 | ## How To Use 122 | 123 | You can set up IRSA for any Kubernetes ServiceAccount by configuring the necessary IAM roles and policies. 124 | While you can use the provided IRSA custom resources, it is also possible to set up IRSA manually by configuring the `iamRole`, `iamPolicies`, and `ServiceAccount` directly. 125 | 126 | ### Using IRSA Custom Resources 127 | 128 | ![](docs/IRSA-cr.png) 129 | 130 | The following example shows how irsa-manager sets up the `irsa1-sa` ServiceAccount in the `kube-system` and `default` namespaces with the AmazonS3FullAccess policy using IRSA custom resources: 131 | 132 | ```yaml 133 | apiVersion: irsa-manager.kkb0318.github.io/v1alpha1 134 | kind: IRSA 135 | metadata: 136 | name: irsa-sample 137 | namespace: irsa-manager-system 138 | spec: 139 | cleanup: true 140 | serviceAccount: 141 | name: irsa1-sa 142 | namespaces: 143 | - kube-system 144 | - default 145 | iamRole: 146 | name: irsa1-role 147 | iamPolicies: 148 | - AmazonS3FullAccess 149 | ``` 150 | 151 | This configuration simplifies the setup process by combining the creation of the IAM role, policies, and service account into a single custom resource. 152 | 153 | ### Manual setup 154 | 155 | Alternatively, you can configure IRSA manually without using the IRSA custom resources by following these steps: 156 | 157 | - Create the IAM Role: 158 | - Manually create an IAM role in AWS with the necessary trust policy to allow the Kubernetes service account to assume the role. 159 | 160 | ```json 161 | { 162 | "Version": "2012-10-17", 163 | "Statement": [ 164 | { 165 | "Effect": "Allow", 166 | "Principal": { 167 | "Federated": "arn:aws:iam:::oidc-provider/s3-.amazonaws.com/" 168 | }, 169 | "Action": "sts:AssumeRoleWithWebIdentity", 170 | "Condition": { 171 | "StringEquals": { 172 | "s3-.amazonaws.com/:sub": "system:serviceaccount::" 173 | } 174 | } 175 | } 176 | ] 177 | } 178 | ``` 179 | 180 | - Attach IAM Policies: 181 | - Attach the required IAM policies (e.g., AmazonS3FullAccess) to the IAM role. 182 | - Annotate the Kubernetes ServiceAccount: 183 | - Annotate the Kubernetes service account with the ARN of the IAM role. 184 | 185 | ```yaml 186 | apiVersion: v1 187 | kind: ServiceAccount 188 | metadata: 189 | name: 190 | namespace: 191 | annotations: 192 | eks.amazonaws.com/role-arn: arn:aws:iam:::role/ 193 | ``` 194 | 195 | ## Verification 196 | 197 | To verify the above example and ensure the IRSA works correctly, you can check the following job. 198 | There is a Kubernetes job that will put one file into the S3 bucket, confirming that the Pod can assume the role to get S3 write permission: 199 | 200 | ```bash 201 | cd validation 202 | sh s3-echoer.sh 203 | ``` 204 | 205 | ## API Reference 206 | 207 | You can find the reference in the [Reference](./docs/api.md) file. 208 | 209 | ## License 210 | 211 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. 212 | 213 | ## Acknowledgments 214 | 215 | In creating this OSS project, I referred to several sources and would like to express my gratitude for their valuable information and insights. 216 | 217 | The necessity of this project was realized through discussions in the following issue: 218 | 219 | - https://github.com/kubernetes-sigs/cluster-api-provider-aws/issues/3560 220 | 221 | Additionally, the implementation was guided by the following repositories: 222 | 223 | - [smalltown/aws-irsa-example](https://github.com/smalltown/aws-irsa-example) 224 | - [aws/amazon-eks-pod-identity-webhook](https://github.com/aws/amazon-eks-pod-identity-webhook) 225 | -------------------------------------------------------------------------------- /api/v1alpha1/conditions.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | const ( 4 | // ReadyCondition indicates the resource is ready and fully reconciled. 5 | // If the Condition is False, the resource SHOULD be considered to be in the process of reconciling and not a 6 | // representation of actual state. 7 | ReadyCondition string = "Ready" 8 | ) 9 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the irsa v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=irsa-manager.kkb0318.github.io 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "irsa-manager.kkb0318.github.io", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1alpha1/irsa_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "slices" 21 | 22 | apimeta "k8s.io/apimachinery/pkg/api/meta" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/types" 25 | ) 26 | 27 | const ( 28 | // IRSAKind represents the kind attribute of an IRSA resource. 29 | IRSAKind = "IRSA" 30 | ) 31 | 32 | // IRSASpec defines the desired state of IRSA 33 | type IRSASpec struct { 34 | // Cleanup, when enabled, allows the IRSA to perform garbage collection 35 | // of resources that are no longer needed or managed. 36 | // +required 37 | Cleanup bool `json:"cleanup"` 38 | 39 | // ServiceAccount represents the Kubernetes service account associated with the IRSA. 40 | // +required 41 | ServiceAccount IRSAServiceAccount `json:"serviceAccount,omitempty"` 42 | 43 | // IamRole represents the IAM role details associated with the IRSA. 44 | // +required 45 | IamRole IamRole `json:"iamRole,omitempty"` 46 | 47 | // IamPolicies represents the list of IAM policies to be attached to the IAM role. 48 | // You can set both the policy name (only AWS default policies) or the full ARN. 49 | // +required 50 | IamPolicies []string `json:"iamPolicies,omitempty"` 51 | } 52 | 53 | // IRSAServiceAccount represents the details of the Kubernetes service account 54 | type IRSAServiceAccount struct { 55 | // Name represents the name of the Kubernetes service account 56 | Name string `json:"name,omitempty"` 57 | // Namespaces represents the list of namespaces where the service account is used 58 | Namespaces []string `json:"namespaces,omitempty"` 59 | } 60 | 61 | // NamespacedNameList returns a slice of types.NamespacedName constructed from the Name and Namespace settings. 62 | func (sa *IRSAServiceAccount) NamespacedNameList() []types.NamespacedName { 63 | namespacedName := make([]types.NamespacedName, len(sa.Namespaces)) 64 | for i, ns := range sa.Namespaces { 65 | namespacedName[i] = types.NamespacedName{ 66 | Name: sa.Name, 67 | Namespace: ns, 68 | } 69 | } 70 | return namespacedName 71 | } 72 | 73 | // IamRole represents the IAM role configuration 74 | type IamRole struct { 75 | // Name represents the name of the IAM role. 76 | Name string `json:"name,omitempty"` 77 | } 78 | 79 | // IRSAStatus defines the observed state of IRSA. 80 | type IRSAStatus struct { 81 | Conditions []metav1.Condition `json:"conditions,omitempty"` 82 | // Inventory of applied service resources 83 | ServiceAccounts StatusServiceAccountList `json:"serviceAccounts,omitempty"` 84 | } 85 | 86 | type StatusServiceAccountList []IRSANamespacedNameWithTags 87 | 88 | func (s *StatusServiceAccountList) IsExist(nsNames types.NamespacedName) bool { 89 | return slices.ContainsFunc(*s, func(sa IRSANamespacedNameWithTags) bool { 90 | return sa.Name == nsNames.Name && sa.Namespace == nsNames.Namespace 91 | }) 92 | } 93 | 94 | // Append adds a new IRSANamespacedNameWithTags to the StatusServiceAccountList. 95 | // If the provided NamespacedName already exists in the list, it will be ignored. 96 | func (s *StatusServiceAccountList) Append(nsNames types.NamespacedName) { 97 | *s = append(*s, IRSANamespacedNameWithTags{ 98 | Name: nsNames.Name, 99 | Namespace: nsNames.Namespace, 100 | }, 101 | ) 102 | } 103 | 104 | // Delete removes an IRSANamespacedNameWithTags from the StatusServiceAccountList 105 | // that matches the provided NamespacedName. If the provided NamespacedName does 106 | // not exist in the list, the method does nothing. 107 | func (s *StatusServiceAccountList) Delete(nsNames types.NamespacedName) { 108 | index := slices.IndexFunc(*s, func(sa IRSANamespacedNameWithTags) bool { 109 | return sa.Name == nsNames.Name && sa.Namespace == nsNames.Namespace 110 | }) 111 | if index != -1 { 112 | *s = slices.Delete(*s, index, index+1) 113 | } 114 | } 115 | 116 | // IRSANamespacedNameWithTags is like a types.NamespacedName with JSON tags 117 | type IRSANamespacedNameWithTags struct { 118 | Name string `json:"name"` 119 | Namespace string `json:"namespace"` 120 | } 121 | 122 | func (s *IRSAStatus) ServiceNamespacedNameList() []types.NamespacedName { 123 | namespacedNameList := make([]types.NamespacedName, len(s.ServiceAccounts)) 124 | for i, n := range s.ServiceAccounts { 125 | namespacedNameList[i] = types.NamespacedName{ 126 | Name: n.Name, 127 | Namespace: n.Namespace, 128 | } 129 | } 130 | return namespacedNameList 131 | } 132 | 133 | // GetIRSAStatusServiceAccounts returns a pointer to the ServiceAccount slice 134 | func (in *IRSA) GetIRSAStatusServiceAccounts() *StatusServiceAccountList { 135 | return &in.Status.ServiceAccounts 136 | } 137 | 138 | // GetIRSAStatusConditions returns a pointer to the Conditions slice 139 | func (in *IRSA) GetIRSAStatusConditions() *[]metav1.Condition { 140 | return &in.Status.Conditions 141 | } 142 | 143 | func IRSAStatusReady(irsa IRSA, reason, message string) IRSA { 144 | newCondition := metav1.Condition{ 145 | Type: ReadyCondition, 146 | Status: metav1.ConditionTrue, 147 | Reason: reason, 148 | Message: message, 149 | } 150 | apimeta.SetStatusCondition(irsa.GetIRSAStatusConditions(), newCondition) 151 | return irsa 152 | } 153 | 154 | func IRSAStatusNotReady(irsa IRSA, reason, message string) IRSA { 155 | newCondition := metav1.Condition{ 156 | Type: ReadyCondition, 157 | Status: metav1.ConditionFalse, 158 | Reason: reason, 159 | Message: message, 160 | } 161 | apimeta.SetStatusCondition(irsa.GetIRSAStatusConditions(), newCondition) 162 | return irsa 163 | } 164 | 165 | func IRSAStatusSetServiceAccount(irsa IRSA, namespacedNames []types.NamespacedName) IRSA { 166 | for _, namespacedName := range namespacedNames { 167 | setStatusServiceAccounts(irsa.GetIRSAStatusServiceAccounts(), namespacedName) 168 | } 169 | return irsa 170 | } 171 | 172 | func IRSAStatusRemoveServiceAccount(irsa IRSA, namespacedNames []types.NamespacedName) IRSA { 173 | for _, namespacedName := range namespacedNames { 174 | removeStatusServiceAccounts(irsa.GetIRSAStatusServiceAccounts(), namespacedName) 175 | } 176 | return irsa 177 | } 178 | 179 | func setStatusServiceAccounts(s *StatusServiceAccountList, namespacedName types.NamespacedName) { 180 | if !s.IsExist(namespacedName) { 181 | s.Append(namespacedName) 182 | } 183 | } 184 | 185 | func removeStatusServiceAccounts(s *StatusServiceAccountList, namespacedName types.NamespacedName) { 186 | if s.IsExist(namespacedName) { 187 | s.Delete(namespacedName) 188 | } 189 | } 190 | 191 | type IRSAReason string 192 | 193 | const ( 194 | IRSAReasonFailedRoleUpdate IRSAReason = "IRSAFailedRoleUpdate" 195 | IRSAReasonFailedK8sApply IRSAReason = "IRSAFailedApplyingResources" 196 | IRSAReasonFailedK8sCleanUp IRSAReason = "IRSAFailedDeletingResources" 197 | IRSAReasonReady IRSAReason = "IRSAReady" 198 | ) 199 | 200 | //+kubebuilder:object:root=true 201 | //+kubebuilder:subresource:status 202 | //+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" 203 | 204 | // IRSA is the Schema for the irsas API 205 | type IRSA struct { 206 | metav1.TypeMeta `json:",inline"` 207 | metav1.ObjectMeta `json:"metadata,omitempty"` 208 | 209 | Spec IRSASpec `json:"spec,omitempty"` 210 | Status IRSAStatus `json:"status,omitempty"` 211 | } 212 | 213 | //+kubebuilder:object:root=true 214 | 215 | // IRSAList contains a list of IRSA 216 | type IRSAList struct { 217 | metav1.TypeMeta `json:",inline"` 218 | metav1.ListMeta `json:"metadata,omitempty"` 219 | Items []IRSA `json:"items"` 220 | } 221 | 222 | func init() { 223 | SchemeBuilder.Register(&IRSA{}, &IRSAList{}) 224 | } 225 | -------------------------------------------------------------------------------- /api/v1alpha1/irsa_types_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/apimachinery/pkg/types" 8 | ) 9 | 10 | func TestStatusServiceAccountList_Append(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | initial StatusServiceAccountList 14 | toAppend types.NamespacedName 15 | expected StatusServiceAccountList 16 | }{ 17 | { 18 | name: "Append new item", 19 | initial: StatusServiceAccountList{ 20 | {Name: "existing", Namespace: "default"}, 21 | }, 22 | toAppend: types.NamespacedName{Name: "new", Namespace: "default"}, 23 | expected: StatusServiceAccountList{ 24 | {Name: "existing", Namespace: "default"}, 25 | {Name: "new", Namespace: "default"}, 26 | }, 27 | }, 28 | { 29 | name: "Append existing item", 30 | initial: StatusServiceAccountList{ 31 | {Name: "existing", Namespace: "default"}, 32 | }, 33 | toAppend: types.NamespacedName{Name: "existing", Namespace: "default"}, 34 | expected: StatusServiceAccountList{ 35 | {Name: "existing", Namespace: "default"}, 36 | {Name: "existing", Namespace: "default"}, 37 | }, 38 | }, 39 | } 40 | 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | tt.initial.Append(tt.toAppend) 44 | assert.Equal(t, tt.expected, tt.initial) 45 | }) 46 | } 47 | } 48 | 49 | func TestStatusServiceAccountList_Delete(t *testing.T) { 50 | tests := []struct { 51 | name string 52 | initial StatusServiceAccountList 53 | toDelete types.NamespacedName 54 | expected StatusServiceAccountList 55 | }{ 56 | { 57 | name: "Delete existing item", 58 | initial: StatusServiceAccountList{ 59 | {Name: "existing", Namespace: "default"}, 60 | {Name: "todelete", Namespace: "default"}, 61 | }, 62 | toDelete: types.NamespacedName{Name: "todelete", Namespace: "default"}, 63 | expected: StatusServiceAccountList{ 64 | {Name: "existing", Namespace: "default"}, 65 | }, 66 | }, 67 | { 68 | name: "Delete non-existing item", 69 | initial: StatusServiceAccountList{ 70 | {Name: "existing", Namespace: "default"}, 71 | }, 72 | toDelete: types.NamespacedName{Name: "nonexisting", Namespace: "default"}, 73 | expected: StatusServiceAccountList{ 74 | {Name: "existing", Namespace: "default"}, 75 | }, 76 | }, 77 | } 78 | 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | tt.initial.Delete(tt.toDelete) 82 | assert.Equal(t, tt.expected, tt.initial) 83 | }) 84 | } 85 | } 86 | 87 | func TestStatusServiceAccountList_IsExist(t *testing.T) { 88 | tests := []struct { 89 | name string 90 | initial StatusServiceAccountList 91 | toCheck types.NamespacedName 92 | expected bool 93 | }{ 94 | { 95 | name: "Item exists", 96 | initial: StatusServiceAccountList{ 97 | {Name: "existing", Namespace: "default"}, 98 | {Name: "another", Namespace: "default"}, 99 | }, 100 | toCheck: types.NamespacedName{Name: "existing", Namespace: "default"}, 101 | expected: true, 102 | }, 103 | { 104 | name: "Item does not exist", 105 | initial: StatusServiceAccountList{ 106 | {Name: "existing", Namespace: "default"}, 107 | }, 108 | toCheck: types.NamespacedName{Name: "nonexisting", Namespace: "default"}, 109 | expected: false, 110 | }, 111 | { 112 | name: "Item with different namespace", 113 | initial: StatusServiceAccountList{ 114 | {Name: "existing", Namespace: "default"}, 115 | }, 116 | toCheck: types.NamespacedName{Name: "existing", Namespace: "othernamespace"}, 117 | expected: false, 118 | }, 119 | } 120 | 121 | for _, tt := range tests { 122 | t.Run(tt.name, func(t *testing.T) { 123 | result := tt.initial.IsExist(tt.toCheck) 124 | assert.Equal(t, tt.expected, result) 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /api/v1alpha1/irsasetup_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | apimeta "k8s.io/apimachinery/pkg/api/meta" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | const ( 25 | // IRSASetupKind represents the kind attribute of an IRSASetup resource. 26 | IRSASetupKind = "IRSASetup" 27 | ) 28 | 29 | // IRSASetupSpec defines the desired state of IRSASetup 30 | type IRSASetupSpec struct { 31 | // Cleanup, when enabled, allows the IRSASetup to perform garbage collection 32 | // of resources that are no longer needed or managed. 33 | // +required 34 | Cleanup bool `json:"cleanup"` 35 | 36 | // Mode specifies the operation mode of the controller. 37 | // Possible values: 38 | // - "selfhosted": For self-managed Kubernetes clusters. 39 | // - "eks": For Amazon EKS environments. 40 | // Default: "selfhosted" 41 | Mode SetupMode `json:"mode,omitempty"` 42 | 43 | // Discovery configures the IdP Discovery process, essential for setting up IRSA by locating 44 | // the OIDC provider information. 45 | // Only applicable when Mode is "selfhosted". 46 | Discovery Discovery `json:"discovery,omitempty"` 47 | 48 | // IamOIDCProvider configures IAM OIDC IamOIDCProvider Name 49 | // Only applicable when Mode is "eks". 50 | IamOIDCProvider string `json:"iamOIDCProvider,omitempty"` 51 | } 52 | 53 | // +kubebuilder:default=selfhosted 54 | // +kubebuilder:validation:Enum=selfhosted;eks 55 | // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" 56 | type SetupMode string 57 | 58 | const ( 59 | ModeSelfhosted = SetupMode("selfhosted") 60 | ModeEks = SetupMode("eks") 61 | ) 62 | 63 | // Discovery holds the configuration for IdP Discovery, which is crucial for locating 64 | // the OIDC provider in a self-hosted environment. 65 | type Discovery struct { 66 | // S3 specifies the AWS S3 bucket details where the OIDC provider's discovery information is hosted. 67 | S3 S3Discovery `json:"s3,omitempty"` 68 | } 69 | 70 | // S3Discovery contains the specifics of the S3 bucket used for hosting OIDC provider discovery information. 71 | type S3Discovery struct { 72 | // Region denotes the AWS region where the S3 bucket is located. 73 | Region string `json:"region"` 74 | 75 | // BucketName is the name of the S3 bucket that hosts the OIDC discovery information. 76 | BucketName string `json:"bucketName"` 77 | } 78 | 79 | // IRSASetupStatus defines the observed state of IRSASetup 80 | type IRSASetupStatus struct { 81 | Conditions []metav1.Condition `json:"conditions,omitempty"` 82 | } 83 | 84 | // GetStatusConditions returns a pointer to the Status.Conditions slice 85 | func (in *IRSASetup) GetStatusConditions() *[]metav1.Condition { 86 | return &in.Status.Conditions 87 | } 88 | 89 | func SetupStatusReady(irsa IRSASetup, reason, message string) IRSASetup { 90 | newCondition := metav1.Condition{ 91 | Type: ReadyCondition, 92 | Status: metav1.ConditionTrue, 93 | Reason: reason, 94 | Message: message, 95 | } 96 | apimeta.SetStatusCondition(irsa.GetStatusConditions(), newCondition) 97 | return irsa 98 | } 99 | 100 | func StatusNotReady(irsa IRSASetup, reason, message string) IRSASetup { 101 | newCondition := metav1.Condition{ 102 | Type: ReadyCondition, 103 | Status: metav1.ConditionFalse, 104 | Reason: reason, 105 | Message: message, 106 | } 107 | apimeta.SetStatusCondition(irsa.GetStatusConditions(), newCondition) 108 | return irsa 109 | } 110 | 111 | // ReadyStatus 112 | func ReadyStatus(irsa IRSASetup) *metav1.Condition { 113 | if c := apimeta.FindStatusCondition(irsa.Status.Conditions, ReadyCondition); c != nil { 114 | return c 115 | } 116 | return nil 117 | } 118 | 119 | // HasConditionReason 120 | func HasConditionReason(cond *metav1.Condition, reasons ...string) bool { 121 | if cond == nil { 122 | return false 123 | } 124 | for _, reason := range reasons { 125 | if cond.Reason == reason { 126 | return true 127 | } 128 | } 129 | return false 130 | } 131 | 132 | func IsReadyConditionTrue(irsa IRSASetup) bool { 133 | return apimeta.IsStatusConditionTrue(irsa.Status.Conditions, ReadyCondition) 134 | } 135 | 136 | type SelfhostedConditionReason string 137 | 138 | const ( 139 | SelfHostedReasonFailedWebhook SelfhostedConditionReason = "SelfHostedSetupFailedWebhookCreation" 140 | SelfHostedReasonFailedOidc SelfhostedConditionReason = "SelfHostedSetupFailedOidcCreation" 141 | SelfHostedReasonFailedIssuer SelfhostedConditionReason = "SelfHostedSetupFailedIssuer" 142 | SelfHostedReasonFailedKeys SelfhostedConditionReason = "SelfHostedSetupFailedKeysCreation" 143 | SelfHostedReasonReady SelfhostedConditionReason = "SelfHostedSetupReady" 144 | ) 145 | 146 | type EksConditionReason string 147 | 148 | const ( 149 | EksNotReady EksConditionReason = "EksOIDCNotReady" 150 | EksReasonReady EksConditionReason = "EksOIDCSetupReady" 151 | ) 152 | 153 | //+kubebuilder:object:root=true 154 | //+kubebuilder:subresource:status 155 | //+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" 156 | 157 | // IRSASetup represents a configuration for setting up IAM Roles for Service Accounts (IRSA) in a Kubernetes cluster. 158 | type IRSASetup struct { 159 | metav1.TypeMeta `json:",inline"` 160 | metav1.ObjectMeta `json:"metadata,omitempty"` 161 | 162 | Spec IRSASetupSpec `json:"spec,omitempty"` 163 | Status IRSASetupStatus `json:"status,omitempty"` 164 | } 165 | 166 | //+kubebuilder:object:root=true 167 | 168 | // IRSASetupList contains a list of IRSASetup 169 | type IRSASetupList struct { 170 | metav1.TypeMeta `json:",inline"` 171 | metav1.ListMeta `json:"metadata,omitempty"` 172 | Items []IRSASetup `json:"items"` 173 | } 174 | 175 | func init() { 176 | SchemeBuilder.Register(&IRSASetup{}, &IRSASetupList{}) 177 | } 178 | -------------------------------------------------------------------------------- /charts/irsa-manager/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/irsa-manager/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: irsa-manager 3 | description: IRSA for non-EKS cluster 4 | # A chart can be either an 'application' or a 'library' chart. 5 | # 6 | # Application charts are a collection of templates that can be packaged into versioned archives 7 | # to be deployed. 8 | # 9 | # Library charts provide useful utilities or functions for the chart developer. They're included as 10 | # a dependency of application charts to inject those utilities and functions into the rendering 11 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 12 | type: application 13 | # This is the chart version. This version number should be incremented each time you make changes 14 | # to the chart and its templates, including the app version. 15 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 16 | version: CHART_VERSION 17 | # This is the version number of the application being deployed. This version number should be 18 | # incremented each time you make changes to the application. Versions are not expected to 19 | # follow Semantic Versioning. They should reflect the version the application is using. 20 | # It is recommended to use it with quotes. 21 | appVersion: APP_VERSION 22 | 23 | keywords: 24 | - kubernetes 25 | - irsa 26 | - operator-manager 27 | - operator 28 | - controller 29 | home: https://github.com/kkb0318/irsa-manager 30 | sources: 31 | - https://github.com/kkb0318/irsa-manager 32 | maintainers: 33 | - name: kkb 34 | email: nkkb0318@gmail.com 35 | annotations: 36 | artifacthub.io/prerelease: "false" 37 | -------------------------------------------------------------------------------- /charts/irsa-manager/README.md: -------------------------------------------------------------------------------- 1 | # irsa-manager 2 | 3 | [irsa-manager](https://github.com/kkb0318/irsa-manager) IRSA manager allows you to easily set up IAM Roles for Service Accounts (IRSA) on non-EKS Kubernetes clusters. 4 | 5 | ## Setup 6 | 7 | * Get Repo Info 8 | ```console 9 | helm repo add kkb0318 https://kkb0318.github.io/irsa-manager 10 | helm repo update 11 | ``` 12 | 13 | * Install Chart 14 | 15 | ```console 16 | helm install irsa-manager kkb0318/irsa-manager -n irsa-manager-system --create-namespace 17 | ``` 18 | 19 | * Set AWS Secret for IRSA Manager 20 | 21 | ```console 22 | kubectl create secret generic aws-secret -n irsa-manager-system \ 23 | --from-literal=aws-access-key-id= \ 24 | --from-literal=aws-secret-access-key= \ 25 | --from-literal=aws-session-token= # Optional \ 26 | --from-literal=aws-region= \ 27 | --from-literal=aws-role-arn= # Optional: Set this if you want to switch roles 28 | 29 | ``` 30 | 31 | ## Values 32 | 33 | | Key | Type | Default | Description | 34 | |-----|------|---------|-------------| 35 | | controllerManager.manager.args[0] | string | `"--leader-elect"` | | 36 | | controllerManager.manager.containerSecurityContext.allowPrivilegeEscalation | bool | `false` | | 37 | | controllerManager.manager.containerSecurityContext.capabilities.drop[0] | string | `"ALL"` | | 38 | | controllerManager.manager.image.repository | string | `"ghcr.io/kkb0318/irsa-manager"` | | 39 | | controllerManager.manager.image.tag | string | `"APP_VERSION"` | | 40 | | controllerManager.manager.resources.limits.cpu | string | `"500m"` | | 41 | | controllerManager.manager.resources.limits.memory | string | `"128Mi"` | | 42 | | controllerManager.manager.resources.requests.cpu | string | `"10m"` | | 43 | | controllerManager.manager.resources.requests.memory | string | `"64Mi"` | | 44 | | controllerManager.replicas | int | `1` | | 45 | | controllerManager.serviceAccount.annotations | object | `{}` | | 46 | | kubernetesClusterDomain | string | `"cluster.local"` | | 47 | | metricsService.ports[0].name | string | `"https"` | | 48 | | metricsService.ports[0].port | int | `8443` | | 49 | | metricsService.ports[0].protocol | string | `"TCP"` | | 50 | | metricsService.ports[0].targetPort | string | `"https"` | | 51 | | metricsService.type | string | `"ClusterIP"` | | 52 | 53 | -------------------------------------------------------------------------------- /charts/irsa-manager/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | {{ template "chart.header" . }} 2 | 3 | [irsa-manager](https://github.com/kkb0318/irsa-manager) IRSA manager allows you to easily set up IAM Roles for Service Accounts (IRSA) on non-EKS Kubernetes clusters. 4 | 5 | ## Setup 6 | 7 | * Get Repo Info 8 | ```console 9 | helm repo add kkb0318 https://kkb0318.github.io/irsa-manager 10 | helm repo update 11 | ``` 12 | 13 | * Install Chart 14 | 15 | ```console 16 | helm install irsa-manager kkb0318/irsa-manager -n irsa-manager-system --create-namespace 17 | ``` 18 | 19 | * Set AWS Secret for IRSA Manager 20 | 21 | ```console 22 | kubectl create secret generic aws-secret -n irsa-manager-system \ 23 | --from-literal=aws-access-key-id= \ 24 | --from-literal=aws-secret-access-key= \ 25 | --from-literal=aws-session-token= # Optional \ 26 | --from-literal=aws-region= \ 27 | --from-literal=aws-role-arn= # Optional: Set this if you want to switch roles 28 | 29 | ``` 30 | 31 | {{ template "chart.valuesSection" . }} 32 | 33 | -------------------------------------------------------------------------------- /charts/irsa-manager/crds/irsa-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.14.0 6 | name: irsas.irsa-manager.kkb0318.github.io 7 | spec: 8 | group: irsa-manager.kkb0318.github.io 9 | names: 10 | kind: IRSA 11 | listKind: IRSAList 12 | plural: irsas 13 | singular: irsa 14 | scope: Namespaced 15 | versions: 16 | - additionalPrinterColumns: 17 | - jsonPath: .status.conditions[?(@.type=="Ready")].status 18 | name: Ready 19 | type: string 20 | name: v1alpha1 21 | schema: 22 | openAPIV3Schema: 23 | description: IRSA is the Schema for the irsas API 24 | properties: 25 | apiVersion: 26 | description: |- 27 | APIVersion defines the versioned schema of this representation of an object. 28 | Servers should convert recognized schemas to the latest internal value, and 29 | may reject unrecognized values. 30 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 31 | type: string 32 | kind: 33 | description: |- 34 | Kind is a string value representing the REST resource this object represents. 35 | Servers may infer this from the endpoint the client submits requests to. 36 | Cannot be updated. 37 | In CamelCase. 38 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 39 | type: string 40 | metadata: 41 | type: object 42 | spec: 43 | description: IRSASpec defines the desired state of IRSA 44 | properties: 45 | cleanup: 46 | description: |- 47 | Cleanup, when enabled, allows the IRSA to perform garbage collection 48 | of resources that are no longer needed or managed. 49 | type: boolean 50 | iamPolicies: 51 | description: |- 52 | IamPolicies represents the list of IAM policies to be attached to the IAM role. 53 | You can set both the policy name (only AWS default policies) or the full ARN. 54 | items: 55 | type: string 56 | type: array 57 | iamRole: 58 | description: IamRole represents the IAM role details associated with 59 | the IRSA. 60 | properties: 61 | name: 62 | description: Name represents the name of the IAM role. 63 | type: string 64 | type: object 65 | serviceAccount: 66 | description: ServiceAccount represents the Kubernetes service account 67 | associated with the IRSA. 68 | properties: 69 | name: 70 | description: Name represents the name of the Kubernetes service 71 | account 72 | type: string 73 | namespaces: 74 | description: Namespaces represents the list of namespaces where 75 | the service account is used 76 | items: 77 | type: string 78 | type: array 79 | type: object 80 | required: 81 | - cleanup 82 | type: object 83 | status: 84 | description: IRSAStatus defines the observed state of IRSA. 85 | properties: 86 | conditions: 87 | items: 88 | description: "Condition contains details for one aspect of the current 89 | state of this API Resource.\n---\nThis struct is intended for 90 | direct use as an array at the field path .status.conditions. For 91 | example,\n\n\n\ttype FooStatus struct{\n\t // Represents the 92 | observations of a foo's current state.\n\t // Known .status.conditions.type 93 | are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // 94 | +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t 95 | \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" 96 | patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t 97 | \ // other fields\n\t}" 98 | properties: 99 | lastTransitionTime: 100 | description: |- 101 | lastTransitionTime is the last time the condition transitioned from one status to another. 102 | This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 103 | format: date-time 104 | type: string 105 | message: 106 | description: |- 107 | message is a human readable message indicating details about the transition. 108 | This may be an empty string. 109 | maxLength: 32768 110 | type: string 111 | observedGeneration: 112 | description: |- 113 | observedGeneration represents the .metadata.generation that the condition was set based upon. 114 | For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 115 | with respect to the current state of the instance. 116 | format: int64 117 | minimum: 0 118 | type: integer 119 | reason: 120 | description: |- 121 | reason contains a programmatic identifier indicating the reason for the condition's last transition. 122 | Producers of specific condition types may define expected values and meanings for this field, 123 | and whether the values are considered a guaranteed API. 124 | The value should be a CamelCase string. 125 | This field may not be empty. 126 | maxLength: 1024 127 | minLength: 1 128 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 129 | type: string 130 | status: 131 | description: status of the condition, one of True, False, Unknown. 132 | enum: 133 | - "True" 134 | - "False" 135 | - Unknown 136 | type: string 137 | type: 138 | description: |- 139 | type of condition in CamelCase or in foo.example.com/CamelCase. 140 | --- 141 | Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be 142 | useful (see .node.status.conditions), the ability to deconflict is important. 143 | The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 144 | maxLength: 316 145 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 146 | type: string 147 | required: 148 | - lastTransitionTime 149 | - message 150 | - reason 151 | - status 152 | - type 153 | type: object 154 | type: array 155 | serviceAccounts: 156 | description: Inventory of applied service resources 157 | items: 158 | description: IRSANamespacedNameWithTags is like a types.NamespacedName 159 | with JSON tags 160 | properties: 161 | name: 162 | type: string 163 | namespace: 164 | type: string 165 | required: 166 | - name 167 | - namespace 168 | type: object 169 | type: array 170 | type: object 171 | type: object 172 | served: true 173 | storage: true 174 | subresources: 175 | status: {} 176 | -------------------------------------------------------------------------------- /charts/irsa-manager/crds/irsasetup-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.14.0 6 | name: irsasetups.irsa-manager.kkb0318.github.io 7 | spec: 8 | group: irsa-manager.kkb0318.github.io 9 | names: 10 | kind: IRSASetup 11 | listKind: IRSASetupList 12 | plural: irsasetups 13 | singular: irsasetup 14 | scope: Namespaced 15 | versions: 16 | - additionalPrinterColumns: 17 | - jsonPath: .status.conditions[?(@.type=="Ready")].status 18 | name: Ready 19 | type: string 20 | name: v1alpha1 21 | schema: 22 | openAPIV3Schema: 23 | description: IRSASetup represents a configuration for setting up IAM Roles 24 | for Service Accounts (IRSA) in a Kubernetes cluster. 25 | properties: 26 | apiVersion: 27 | description: |- 28 | APIVersion defines the versioned schema of this representation of an object. 29 | Servers should convert recognized schemas to the latest internal value, and 30 | may reject unrecognized values. 31 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 32 | type: string 33 | kind: 34 | description: |- 35 | Kind is a string value representing the REST resource this object represents. 36 | Servers may infer this from the endpoint the client submits requests to. 37 | Cannot be updated. 38 | In CamelCase. 39 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 40 | type: string 41 | metadata: 42 | type: object 43 | spec: 44 | description: IRSASetupSpec defines the desired state of IRSASetup 45 | properties: 46 | cleanup: 47 | description: |- 48 | Cleanup, when enabled, allows the IRSASetup to perform garbage collection 49 | of resources that are no longer needed or managed. 50 | type: boolean 51 | discovery: 52 | description: |- 53 | Discovery configures the IdP Discovery process, essential for setting up IRSA by locating 54 | the OIDC provider information. 55 | Only applicable when Mode is "selfhosted". 56 | properties: 57 | s3: 58 | description: S3 specifies the AWS S3 bucket details where the 59 | OIDC provider's discovery information is hosted. 60 | properties: 61 | bucketName: 62 | description: BucketName is the name of the S3 bucket that 63 | hosts the OIDC discovery information. 64 | type: string 65 | region: 66 | description: Region denotes the AWS region where the S3 bucket 67 | is located. 68 | type: string 69 | required: 70 | - bucketName 71 | - region 72 | type: object 73 | type: object 74 | iamOIDCProvider: 75 | description: |- 76 | IamOIDCProvider configures IAM OIDC IamOIDCProvider Name 77 | Only applicable when Mode is "eks". 78 | type: string 79 | mode: 80 | description: |- 81 | Mode specifies the operation mode of the controller. 82 | Possible values: 83 | - "selfhosted": For self-managed Kubernetes clusters. 84 | - "eks": For Amazon EKS environments. 85 | Default: "selfhosted" 86 | enum: 87 | - selfhosted 88 | - eks 89 | type: string 90 | x-kubernetes-validations: 91 | - message: Value is immutable 92 | rule: self == oldSelf 93 | required: 94 | - cleanup 95 | type: object 96 | status: 97 | description: IRSASetupStatus defines the observed state of IRSASetup 98 | properties: 99 | conditions: 100 | items: 101 | description: "Condition contains details for one aspect of the current 102 | state of this API Resource.\n---\nThis struct is intended for 103 | direct use as an array at the field path .status.conditions. For 104 | example,\n\n\n\ttype FooStatus struct{\n\t // Represents the 105 | observations of a foo's current state.\n\t // Known .status.conditions.type 106 | are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // 107 | +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t 108 | \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" 109 | patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t 110 | \ // other fields\n\t}" 111 | properties: 112 | lastTransitionTime: 113 | description: |- 114 | lastTransitionTime is the last time the condition transitioned from one status to another. 115 | This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 116 | format: date-time 117 | type: string 118 | message: 119 | description: |- 120 | message is a human readable message indicating details about the transition. 121 | This may be an empty string. 122 | maxLength: 32768 123 | type: string 124 | observedGeneration: 125 | description: |- 126 | observedGeneration represents the .metadata.generation that the condition was set based upon. 127 | For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 128 | with respect to the current state of the instance. 129 | format: int64 130 | minimum: 0 131 | type: integer 132 | reason: 133 | description: |- 134 | reason contains a programmatic identifier indicating the reason for the condition's last transition. 135 | Producers of specific condition types may define expected values and meanings for this field, 136 | and whether the values are considered a guaranteed API. 137 | The value should be a CamelCase string. 138 | This field may not be empty. 139 | maxLength: 1024 140 | minLength: 1 141 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 142 | type: string 143 | status: 144 | description: status of the condition, one of True, False, Unknown. 145 | enum: 146 | - "True" 147 | - "False" 148 | - Unknown 149 | type: string 150 | type: 151 | description: |- 152 | type of condition in CamelCase or in foo.example.com/CamelCase. 153 | --- 154 | Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be 155 | useful (see .node.status.conditions), the ability to deconflict is important. 156 | The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 157 | maxLength: 316 158 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 159 | type: string 160 | required: 161 | - lastTransitionTime 162 | - message 163 | - reason 164 | - status 165 | - type 166 | type: object 167 | type: array 168 | type: object 169 | type: object 170 | served: true 171 | storage: true 172 | subresources: 173 | status: {} 174 | -------------------------------------------------------------------------------- /charts/irsa-manager/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "irsa-manager.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "irsa-manager.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "irsa-manager.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "irsa-manager.labels" -}} 37 | helm.sh/chart: {{ include "irsa-manager.chart" . }} 38 | {{ include "irsa-manager.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "irsa-manager.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "irsa-manager.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "irsa-manager.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "irsa-manager.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /charts/irsa-manager/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "irsa-manager.fullname" . }}-controller-manager 5 | labels: 6 | app.kubernetes.io/component: manager 7 | app.kubernetes.io/created-by: irsa-manager 8 | app.kubernetes.io/part-of: irsa-manager 9 | control-plane: controller-manager 10 | {{- include "irsa-manager.labels" . | nindent 4 }} 11 | spec: 12 | replicas: {{ .Values.controllerManager.replicas }} 13 | selector: 14 | matchLabels: 15 | control-plane: controller-manager 16 | {{- include "irsa-manager.selectorLabels" . | nindent 6 }} 17 | template: 18 | metadata: 19 | labels: 20 | control-plane: controller-manager 21 | {{- include "irsa-manager.selectorLabels" . | nindent 8 }} 22 | annotations: 23 | kubectl.kubernetes.io/default-container: manager 24 | spec: 25 | containers: 26 | - args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} 27 | command: 28 | - /manager 29 | env: 30 | - name: AWS_ACCESS_KEY_ID 31 | valueFrom: 32 | secretKeyRef: 33 | key: aws-access-key-id 34 | name: aws-secret 35 | optional: true 36 | - name: AWS_SECRET_ACCESS_KEY 37 | valueFrom: 38 | secretKeyRef: 39 | key: aws-secret-access-key 40 | name: aws-secret 41 | optional: true 42 | - name: AWS_SESSION_TOKEN 43 | valueFrom: 44 | secretKeyRef: 45 | key: aws-session-token 46 | name: aws-secret 47 | optional: true 48 | - name: AWS_REGION 49 | valueFrom: 50 | secretKeyRef: 51 | key: aws-region 52 | name: aws-secret 53 | optional: true 54 | - name: AWS_ROLE_ARN 55 | valueFrom: 56 | secretKeyRef: 57 | key: aws-role-arn 58 | name: aws-secret 59 | optional: true 60 | - name: KUBERNETES_CLUSTER_DOMAIN 61 | value: {{ quote .Values.kubernetesClusterDomain }} 62 | {{- if .Values.proxy.enabled }} 63 | {{- if .Values.proxy.httpProxy }} 64 | - name: HTTP_PROXY 65 | value: {{ .Values.proxy.httpProxy | quote }} 66 | {{- end }} 67 | {{- if .Values.proxy.httpsProxy }} 68 | - name: HTTPS_PROXY 69 | value: {{ .Values.proxy.httpsProxy | quote }} 70 | {{- end }} 71 | {{- if .Values.proxy.noProxy }} 72 | - name: NO_PROXY 73 | value: {{ .Values.proxy.noProxy | quote }} 74 | {{- end }} 75 | {{- end }} 76 | image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag 77 | | default .Chart.AppVersion }} 78 | livenessProbe: 79 | httpGet: 80 | path: /healthz 81 | port: 8081 82 | initialDelaySeconds: 15 83 | periodSeconds: 20 84 | name: manager 85 | readinessProbe: 86 | httpGet: 87 | path: /readyz 88 | port: 8081 89 | initialDelaySeconds: 5 90 | periodSeconds: 10 91 | resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 92 | }} 93 | securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext 94 | | nindent 10 }} 95 | securityContext: 96 | runAsNonRoot: true 97 | seccompProfile: 98 | type: RuntimeDefault 99 | serviceAccountName: {{ include "irsa-manager.fullname" . }}-controller-manager 100 | terminationGracePeriodSeconds: 10 -------------------------------------------------------------------------------- /charts/irsa-manager/templates/leader-election-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: {{ include "irsa-manager.fullname" . }}-leader-election-role 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: irsa-manager 8 | app.kubernetes.io/part-of: irsa-manager 9 | {{- include "irsa-manager.labels" . | nindent 4 }} 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - configmaps 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | - create 20 | - update 21 | - patch 22 | - delete 23 | - apiGroups: 24 | - coordination.k8s.io 25 | resources: 26 | - leases 27 | verbs: 28 | - get 29 | - list 30 | - watch 31 | - create 32 | - update 33 | - patch 34 | - delete 35 | - apiGroups: 36 | - "" 37 | resources: 38 | - events 39 | verbs: 40 | - create 41 | - patch 42 | --- 43 | apiVersion: rbac.authorization.k8s.io/v1 44 | kind: RoleBinding 45 | metadata: 46 | name: {{ include "irsa-manager.fullname" . }}-leader-election-rolebinding 47 | labels: 48 | app.kubernetes.io/component: rbac 49 | app.kubernetes.io/created-by: irsa-manager 50 | app.kubernetes.io/part-of: irsa-manager 51 | {{- include "irsa-manager.labels" . | nindent 4 }} 52 | roleRef: 53 | apiGroup: rbac.authorization.k8s.io 54 | kind: Role 55 | name: '{{ include "irsa-manager.fullname" . }}-leader-election-role' 56 | subjects: 57 | - kind: ServiceAccount 58 | name: '{{ include "irsa-manager.fullname" . }}-controller-manager' 59 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /charts/irsa-manager/templates/manager-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "irsa-manager.fullname" . }}-manager-role 5 | labels: 6 | {{- include "irsa-manager.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - secrets 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - serviceaccounts 24 | verbs: 25 | - create 26 | - delete 27 | - get 28 | - list 29 | - patch 30 | - update 31 | - watch 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - services 36 | verbs: 37 | - create 38 | - delete 39 | - get 40 | - list 41 | - patch 42 | - update 43 | - watch 44 | - apiGroups: 45 | - admissionregistration.k8s.io 46 | resources: 47 | - mutatingwebhookconfigurations 48 | verbs: 49 | - create 50 | - delete 51 | - get 52 | - list 53 | - patch 54 | - update 55 | - watch 56 | - apiGroups: 57 | - apps 58 | resources: 59 | - deployments 60 | verbs: 61 | - create 62 | - delete 63 | - get 64 | - list 65 | - patch 66 | - update 67 | - watch 68 | - apiGroups: 69 | - certificates.k8s.io 70 | resources: 71 | - certificatesigningrequests 72 | verbs: 73 | - create 74 | - delete 75 | - get 76 | - list 77 | - patch 78 | - update 79 | - watch 80 | - apiGroups: 81 | - irsa-manager.kkb0318.github.io 82 | resources: 83 | - irsas 84 | verbs: 85 | - create 86 | - delete 87 | - get 88 | - list 89 | - patch 90 | - update 91 | - watch 92 | - apiGroups: 93 | - irsa-manager.kkb0318.github.io 94 | resources: 95 | - irsas/finalizers 96 | verbs: 97 | - update 98 | - apiGroups: 99 | - irsa-manager.kkb0318.github.io 100 | resources: 101 | - irsas/status 102 | verbs: 103 | - get 104 | - patch 105 | - update 106 | - apiGroups: 107 | - irsa-manager.kkb0318.github.io 108 | resources: 109 | - irsasetups 110 | verbs: 111 | - create 112 | - delete 113 | - get 114 | - list 115 | - patch 116 | - update 117 | - watch 118 | - apiGroups: 119 | - irsa-manager.kkb0318.github.io 120 | resources: 121 | - irsasetups/finalizers 122 | verbs: 123 | - update 124 | - apiGroups: 125 | - irsa-manager.kkb0318.github.io 126 | resources: 127 | - irsasetups/status 128 | verbs: 129 | - get 130 | - patch 131 | - update 132 | - apiGroups: 133 | - rbac.authorization.k8s.io 134 | resources: 135 | - clusterrolebindings 136 | verbs: 137 | - create 138 | - delete 139 | - get 140 | - list 141 | - patch 142 | - update 143 | - watch 144 | - apiGroups: 145 | - rbac.authorization.k8s.io 146 | resources: 147 | - clusterroles 148 | verbs: 149 | - create 150 | - delete 151 | - get 152 | - list 153 | - patch 154 | - update 155 | - watch 156 | --- 157 | apiVersion: rbac.authorization.k8s.io/v1 158 | kind: ClusterRoleBinding 159 | metadata: 160 | name: {{ include "irsa-manager.fullname" . }}-manager-rolebinding 161 | labels: 162 | app.kubernetes.io/component: rbac 163 | app.kubernetes.io/created-by: irsa-manager 164 | app.kubernetes.io/part-of: irsa-manager 165 | {{- include "irsa-manager.labels" . | nindent 4 }} 166 | roleRef: 167 | apiGroup: rbac.authorization.k8s.io 168 | kind: ClusterRole 169 | name: '{{ include "irsa-manager.fullname" . }}-manager-role' 170 | subjects: 171 | - kind: ServiceAccount 172 | name: '{{ include "irsa-manager.fullname" . }}-controller-manager' 173 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /charts/irsa-manager/templates/metrics-reader-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "irsa-manager.fullname" . }}-metrics-reader 5 | labels: 6 | app.kubernetes.io/component: kube-rbac-proxy 7 | app.kubernetes.io/created-by: irsa-manager 8 | app.kubernetes.io/part-of: irsa-manager 9 | {{- include "irsa-manager.labels" . | nindent 4 }} 10 | rules: 11 | - nonResourceURLs: 12 | - /metrics 13 | verbs: 14 | - get -------------------------------------------------------------------------------- /charts/irsa-manager/templates/metrics-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "irsa-manager.fullname" . }}-controller-manager-metrics-service 5 | labels: 6 | app.kubernetes.io/component: kube-rbac-proxy 7 | app.kubernetes.io/created-by: irsa-manager 8 | app.kubernetes.io/part-of: irsa-manager 9 | control-plane: controller-manager 10 | {{- include "irsa-manager.labels" . | nindent 4 }} 11 | spec: 12 | type: {{ .Values.metricsService.type }} 13 | selector: 14 | control-plane: controller-manager 15 | {{- include "irsa-manager.selectorLabels" . | nindent 4 }} 16 | ports: 17 | {{- .Values.metricsService.ports | toYaml | nindent 2 }} -------------------------------------------------------------------------------- /charts/irsa-manager/templates/proxy-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "irsa-manager.fullname" . }}-proxy-role 5 | labels: 6 | app.kubernetes.io/component: kube-rbac-proxy 7 | app.kubernetes.io/created-by: irsa-manager 8 | app.kubernetes.io/part-of: irsa-manager 9 | {{- include "irsa-manager.labels" . | nindent 4 }} 10 | rules: 11 | - apiGroups: 12 | - authentication.k8s.io 13 | resources: 14 | - tokenreviews 15 | verbs: 16 | - create 17 | - apiGroups: 18 | - authorization.k8s.io 19 | resources: 20 | - subjectaccessreviews 21 | verbs: 22 | - create 23 | --- 24 | apiVersion: rbac.authorization.k8s.io/v1 25 | kind: ClusterRoleBinding 26 | metadata: 27 | name: {{ include "irsa-manager.fullname" . }}-proxy-rolebinding 28 | labels: 29 | app.kubernetes.io/component: kube-rbac-proxy 30 | app.kubernetes.io/created-by: irsa-manager 31 | app.kubernetes.io/part-of: irsa-manager 32 | {{- include "irsa-manager.labels" . | nindent 4 }} 33 | roleRef: 34 | apiGroup: rbac.authorization.k8s.io 35 | kind: ClusterRole 36 | name: '{{ include "irsa-manager.fullname" . }}-proxy-role' 37 | subjects: 38 | - kind: ServiceAccount 39 | name: '{{ include "irsa-manager.fullname" . }}-controller-manager' 40 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /charts/irsa-manager/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "irsa-manager.fullname" . }}-controller-manager 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: irsa-manager 8 | app.kubernetes.io/part-of: irsa-manager 9 | {{- include "irsa-manager.labels" . | nindent 4 }} 10 | annotations: 11 | {{- toYaml .Values.controllerManager.serviceAccount.annotations | nindent 4 }} -------------------------------------------------------------------------------- /charts/irsa-manager/values.yaml: -------------------------------------------------------------------------------- 1 | controllerManager: 2 | manager: 3 | args: 4 | - --leader-elect 5 | containerSecurityContext: 6 | allowPrivilegeEscalation: false 7 | capabilities: 8 | drop: 9 | - ALL 10 | image: 11 | repository: ghcr.io/kkb0318/irsa-manager 12 | tag: APP_VERSION 13 | resources: 14 | limits: 15 | cpu: 500m 16 | memory: 128Mi 17 | requests: 18 | cpu: 10m 19 | memory: 64Mi 20 | replicas: 1 21 | serviceAccount: 22 | annotations: {} 23 | kubernetesClusterDomain: cluster.local 24 | metricsService: 25 | ports: 26 | - name: https 27 | port: 8443 28 | protocol: TCP 29 | targetPort: https 30 | type: ClusterIP 31 | proxy: 32 | enabled: false 33 | httpProxy: "" 34 | httpsProxy: "" 35 | noProxy: "localhost,127.0.0.1," 36 | 37 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "crypto/tls" 21 | "flag" 22 | "os" 23 | 24 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 25 | // to ensure that exec-entrypoint and run can make use of them. 26 | _ "k8s.io/client-go/plugin/pkg/client/auth" 27 | 28 | "k8s.io/apimachinery/pkg/runtime" 29 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 30 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/healthz" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 35 | "sigs.k8s.io/controller-runtime/pkg/webhook" 36 | 37 | irsav1alpha1 "github.com/kkb0318/irsa-manager/api/v1alpha1" 38 | "github.com/kkb0318/irsa-manager/internal/controller" 39 | //+kubebuilder:scaffold:imports 40 | ) 41 | 42 | var ( 43 | scheme = runtime.NewScheme() 44 | setupLog = ctrl.Log.WithName("setup") 45 | ) 46 | 47 | func init() { 48 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 49 | 50 | utilruntime.Must(irsav1alpha1.AddToScheme(scheme)) 51 | //+kubebuilder:scaffold:scheme 52 | } 53 | 54 | func main() { 55 | var metricsAddr string 56 | var enableLeaderElection bool 57 | var probeAddr string 58 | var secureMetrics bool 59 | var enableHTTP2 bool 60 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 61 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 62 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 63 | "Enable leader election for controller manager. "+ 64 | "Enabling this will ensure there is only one active controller manager.") 65 | flag.BoolVar(&secureMetrics, "metrics-secure", false, 66 | "If set the metrics endpoint is served securely") 67 | flag.BoolVar(&enableHTTP2, "enable-http2", false, 68 | "If set, HTTP/2 will be enabled for the metrics and webhook servers") 69 | opts := zap.Options{ 70 | Development: true, 71 | } 72 | opts.BindFlags(flag.CommandLine) 73 | flag.Parse() 74 | 75 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 76 | 77 | // if the enable-http2 flag is false (the default), http/2 should be disabled 78 | // due to its vulnerabilities. More specifically, disabling http/2 will 79 | // prevent from being vulnerable to the HTTP/2 Stream Cancelation and 80 | // Rapid Reset CVEs. For more information see: 81 | // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 82 | // - https://github.com/advisories/GHSA-4374-p667-p6c8 83 | disableHTTP2 := func(c *tls.Config) { 84 | setupLog.Info("disabling http/2") 85 | c.NextProtos = []string{"http/1.1"} 86 | } 87 | 88 | tlsOpts := []func(*tls.Config){} 89 | if !enableHTTP2 { 90 | tlsOpts = append(tlsOpts, disableHTTP2) 91 | } 92 | 93 | webhookServer := webhook.NewServer(webhook.Options{ 94 | TLSOpts: tlsOpts, 95 | }) 96 | 97 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 98 | Scheme: scheme, 99 | Metrics: metricsserver.Options{ 100 | BindAddress: metricsAddr, 101 | SecureServing: secureMetrics, 102 | TLSOpts: tlsOpts, 103 | }, 104 | WebhookServer: webhookServer, 105 | HealthProbeBindAddress: probeAddr, 106 | LeaderElection: enableLeaderElection, 107 | LeaderElectionID: "8ac0b5b6.kkb0318.github.io", 108 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 109 | // when the Manager ends. This requires the binary to immediately end when the 110 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 111 | // speeds up voluntary leader transitions as the new leader don't have to wait 112 | // LeaseDuration time first. 113 | // 114 | // In the default scaffold provided, the program ends immediately after 115 | // the manager stops, so would be fine to enable this option. However, 116 | // if you are doing or is intended to do any operation such as perform cleanups 117 | // after the manager stops then its usage might be unsafe. 118 | // LeaderElectionReleaseOnCancel: true, 119 | }) 120 | if err != nil { 121 | setupLog.Error(err, "unable to start manager") 122 | os.Exit(1) 123 | } 124 | 125 | if err = (&controller.IRSASetupReconciler{ 126 | Client: mgr.GetClient(), 127 | Scheme: mgr.GetScheme(), 128 | }).SetupWithManager(mgr); err != nil { 129 | setupLog.Error(err, "unable to create controller", "controller", "IRSASetup") 130 | os.Exit(1) 131 | } 132 | if err = (&controller.IRSAReconciler{ 133 | Client: mgr.GetClient(), 134 | Scheme: mgr.GetScheme(), 135 | }).SetupWithManager(mgr); err != nil { 136 | setupLog.Error(err, "unable to create controller", "controller", "IRSA") 137 | os.Exit(1) 138 | } 139 | //+kubebuilder:scaffold:builder 140 | 141 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 142 | setupLog.Error(err, "unable to set up health check") 143 | os.Exit(1) 144 | } 145 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 146 | setupLog.Error(err, "unable to set up ready check") 147 | os.Exit(1) 148 | } 149 | 150 | setupLog.Info("starting manager") 151 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 152 | setupLog.Error(err, "problem running manager") 153 | os.Exit(1) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /config/crd/bases/irsa-manager.kkb0318.github.io_irsas.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: irsas.irsa-manager.kkb0318.github.io 8 | spec: 9 | group: irsa-manager.kkb0318.github.io 10 | names: 11 | kind: IRSA 12 | listKind: IRSAList 13 | plural: irsas 14 | singular: irsa 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .status.conditions[?(@.type=="Ready")].status 19 | name: Ready 20 | type: string 21 | name: v1alpha1 22 | schema: 23 | openAPIV3Schema: 24 | description: IRSA is the Schema for the irsas API 25 | properties: 26 | apiVersion: 27 | description: |- 28 | APIVersion defines the versioned schema of this representation of an object. 29 | Servers should convert recognized schemas to the latest internal value, and 30 | may reject unrecognized values. 31 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 32 | type: string 33 | kind: 34 | description: |- 35 | Kind is a string value representing the REST resource this object represents. 36 | Servers may infer this from the endpoint the client submits requests to. 37 | Cannot be updated. 38 | In CamelCase. 39 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 40 | type: string 41 | metadata: 42 | type: object 43 | spec: 44 | description: IRSASpec defines the desired state of IRSA 45 | properties: 46 | cleanup: 47 | description: |- 48 | Cleanup, when enabled, allows the IRSA to perform garbage collection 49 | of resources that are no longer needed or managed. 50 | type: boolean 51 | iamPolicies: 52 | description: |- 53 | IamPolicies represents the list of IAM policies to be attached to the IAM role. 54 | You can set both the policy name (only AWS default policies) or the full ARN. 55 | items: 56 | type: string 57 | type: array 58 | iamRole: 59 | description: IamRole represents the IAM role details associated with 60 | the IRSA. 61 | properties: 62 | name: 63 | description: Name represents the name of the IAM role. 64 | type: string 65 | type: object 66 | serviceAccount: 67 | description: ServiceAccount represents the Kubernetes service account 68 | associated with the IRSA. 69 | properties: 70 | name: 71 | description: Name represents the name of the Kubernetes service 72 | account 73 | type: string 74 | namespaces: 75 | description: Namespaces represents the list of namespaces where 76 | the service account is used 77 | items: 78 | type: string 79 | type: array 80 | type: object 81 | required: 82 | - cleanup 83 | type: object 84 | status: 85 | description: IRSAStatus defines the observed state of IRSA. 86 | properties: 87 | conditions: 88 | items: 89 | description: "Condition contains details for one aspect of the current 90 | state of this API Resource.\n---\nThis struct is intended for 91 | direct use as an array at the field path .status.conditions. For 92 | example,\n\n\n\ttype FooStatus struct{\n\t // Represents the 93 | observations of a foo's current state.\n\t // Known .status.conditions.type 94 | are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // 95 | +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t 96 | \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" 97 | patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t 98 | \ // other fields\n\t}" 99 | properties: 100 | lastTransitionTime: 101 | description: |- 102 | lastTransitionTime is the last time the condition transitioned from one status to another. 103 | This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 104 | format: date-time 105 | type: string 106 | message: 107 | description: |- 108 | message is a human readable message indicating details about the transition. 109 | This may be an empty string. 110 | maxLength: 32768 111 | type: string 112 | observedGeneration: 113 | description: |- 114 | observedGeneration represents the .metadata.generation that the condition was set based upon. 115 | For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 116 | with respect to the current state of the instance. 117 | format: int64 118 | minimum: 0 119 | type: integer 120 | reason: 121 | description: |- 122 | reason contains a programmatic identifier indicating the reason for the condition's last transition. 123 | Producers of specific condition types may define expected values and meanings for this field, 124 | and whether the values are considered a guaranteed API. 125 | The value should be a CamelCase string. 126 | This field may not be empty. 127 | maxLength: 1024 128 | minLength: 1 129 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 130 | type: string 131 | status: 132 | description: status of the condition, one of True, False, Unknown. 133 | enum: 134 | - "True" 135 | - "False" 136 | - Unknown 137 | type: string 138 | type: 139 | description: |- 140 | type of condition in CamelCase or in foo.example.com/CamelCase. 141 | --- 142 | Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be 143 | useful (see .node.status.conditions), the ability to deconflict is important. 144 | The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 145 | maxLength: 316 146 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 147 | type: string 148 | required: 149 | - lastTransitionTime 150 | - message 151 | - reason 152 | - status 153 | - type 154 | type: object 155 | type: array 156 | serviceAccounts: 157 | description: Inventory of applied service resources 158 | items: 159 | description: IRSANamespacedNameWithTags is like a types.NamespacedName 160 | with JSON tags 161 | properties: 162 | name: 163 | type: string 164 | namespace: 165 | type: string 166 | required: 167 | - name 168 | - namespace 169 | type: object 170 | type: array 171 | type: object 172 | type: object 173 | served: true 174 | storage: true 175 | subresources: 176 | status: {} 177 | -------------------------------------------------------------------------------- /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/irsa-manager.kkb0318.github.io_irsasetups.yaml 6 | - bases/irsa-manager.kkb0318.github.io_irsas.yaml 7 | #+kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patches: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | #- path: patches/webhook_in_irsasetups.yaml 13 | #- path: patches/webhook_in_irsas.yaml 14 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 15 | 16 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 17 | # patches here are for enabling the CA injection for each CRD 18 | #- path: patches/cainjection_in_irsasetups.yaml 19 | #- path: patches/cainjection_in_irsas.yaml 20 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 21 | 22 | # [WEBHOOK] To enable webhook, uncomment the following section 23 | # the following config is for teaching kustomize how to do kustomization for CRDs. 24 | 25 | #configurations: 26 | #- kustomizeconfig.yaml 27 | -------------------------------------------------------------------------------- /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 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: irsa-manager-system 2 | 3 | namePrefix: irsa-manager- 4 | 5 | resources: 6 | - ../crd 7 | - ../rbac 8 | - ../manager 9 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: ghcr.io/kkb0318/irsa-manager 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: irsa-manager 10 | app.kubernetes.io/part-of: irsa-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: irsa-manager 25 | app.kubernetes.io/part-of: irsa-manager 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | securityContext: 40 | runAsNonRoot: true 41 | seccompProfile: 42 | type: RuntimeDefault 43 | containers: 44 | - command: 45 | - /manager 46 | args: 47 | - --leader-elect 48 | image: ghcr.io/kkb0318/irsa-manager:APP_VERSION 49 | env: 50 | - name: AWS_ACCESS_KEY_ID 51 | valueFrom: 52 | secretKeyRef: 53 | name: aws-secret 54 | key: aws-access-key-id 55 | optional: true 56 | - name: AWS_SECRET_ACCESS_KEY 57 | valueFrom: 58 | secretKeyRef: 59 | name: aws-secret 60 | key: aws-secret-access-key 61 | optional: true 62 | - name: AWS_SESSION_TOKEN 63 | valueFrom: 64 | secretKeyRef: 65 | name: aws-secret 66 | key: aws-session-token 67 | optional: true 68 | - name: AWS_REGION 69 | valueFrom: 70 | secretKeyRef: 71 | name: aws-secret 72 | key: aws-region 73 | optional: true 74 | - name: AWS_ROLE_ARN 75 | valueFrom: 76 | secretKeyRef: 77 | name: aws-secret 78 | key: aws-role-arn 79 | optional: true 80 | name: manager 81 | securityContext: 82 | allowPrivilegeEscalation: false 83 | capabilities: 84 | drop: 85 | - "ALL" 86 | livenessProbe: 87 | httpGet: 88 | path: /healthz 89 | port: 8081 90 | initialDelaySeconds: 15 91 | periodSeconds: 20 92 | readinessProbe: 93 | httpGet: 94 | path: /readyz 95 | port: 8081 96 | initialDelaySeconds: 5 97 | periodSeconds: 10 98 | resources: 99 | limits: 100 | cpu: 500m 101 | memory: 128Mi 102 | requests: 103 | cpu: 10m 104 | memory: 64Mi 105 | serviceAccountName: controller-manager 106 | terminationGracePeriodSeconds: 10 107 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: irsa-manager 9 | app.kubernetes.io/part-of: irsa-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: irsa-manager 9 | app.kubernetes.io/part-of: irsa-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: irsa-manager 9 | app.kubernetes.io/part-of: irsa-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: irsa-manager 10 | app.kubernetes.io/part-of: irsa-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /config/rbac/irsa_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit irsas. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: irsa-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: irsa-manager 10 | app.kubernetes.io/part-of: irsa-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: irsa-editor-role 13 | rules: 14 | - apiGroups: 15 | - irsa-manager.kkb0318.github.io 16 | resources: 17 | - irsas 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - irsa-manager.kkb0318.github.io 28 | resources: 29 | - irsas/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/irsa_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view irsas. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: irsa-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: irsa-manager 10 | app.kubernetes.io/part-of: irsa-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: irsa-viewer-role 13 | rules: 14 | - apiGroups: 15 | - irsa-manager.kkb0318.github.io 16 | resources: 17 | - irsas 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - irsa-manager.kkb0318.github.io 24 | resources: 25 | - irsas/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/irsasetup_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit irsasetups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: irsasetup-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: irsa-manager 10 | app.kubernetes.io/part-of: irsa-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: irsasetup-editor-role 13 | rules: 14 | - apiGroups: 15 | - irsa-manager.kkb0318.github.io 16 | resources: 17 | - irsasetups 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - irsa-manager.kkb0318.github.io 28 | resources: 29 | - irsasetups/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/irsasetup_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view irsasetups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: irsasetup-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: irsa-manager 10 | app.kubernetes.io/part-of: irsa-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: irsasetup-viewer-role 13 | rules: 14 | - apiGroups: 15 | - irsa-manager.kkb0318.github.io 16 | resources: 17 | - irsasetups 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - irsa-manager.kkb0318.github.io 24 | resources: 25 | - irsasetups/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: irsa-manager 10 | app.kubernetes.io/part-of: irsa-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: irsa-manager 9 | app.kubernetes.io/part-of: irsa-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - secrets 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - serviceaccounts 23 | verbs: 24 | - create 25 | - delete 26 | - get 27 | - list 28 | - patch 29 | - update 30 | - watch 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - services 35 | verbs: 36 | - create 37 | - delete 38 | - get 39 | - list 40 | - patch 41 | - update 42 | - watch 43 | - apiGroups: 44 | - admissionregistration.k8s.io 45 | resources: 46 | - mutatingwebhookconfigurations 47 | verbs: 48 | - create 49 | - delete 50 | - get 51 | - list 52 | - patch 53 | - update 54 | - watch 55 | - apiGroups: 56 | - apps 57 | resources: 58 | - deployments 59 | verbs: 60 | - create 61 | - delete 62 | - get 63 | - list 64 | - patch 65 | - update 66 | - watch 67 | - apiGroups: 68 | - certificates.k8s.io 69 | resources: 70 | - certificatesigningrequests 71 | verbs: 72 | - create 73 | - delete 74 | - get 75 | - list 76 | - patch 77 | - update 78 | - watch 79 | - apiGroups: 80 | - irsa-manager.kkb0318.github.io 81 | resources: 82 | - irsas 83 | verbs: 84 | - create 85 | - delete 86 | - get 87 | - list 88 | - patch 89 | - update 90 | - watch 91 | - apiGroups: 92 | - irsa-manager.kkb0318.github.io 93 | resources: 94 | - irsas/finalizers 95 | verbs: 96 | - update 97 | - apiGroups: 98 | - irsa-manager.kkb0318.github.io 99 | resources: 100 | - irsas/status 101 | verbs: 102 | - get 103 | - patch 104 | - update 105 | - apiGroups: 106 | - irsa-manager.kkb0318.github.io 107 | resources: 108 | - irsasetups 109 | verbs: 110 | - create 111 | - delete 112 | - get 113 | - list 114 | - patch 115 | - update 116 | - watch 117 | - apiGroups: 118 | - irsa-manager.kkb0318.github.io 119 | resources: 120 | - irsasetups/finalizers 121 | verbs: 122 | - update 123 | - apiGroups: 124 | - irsa-manager.kkb0318.github.io 125 | resources: 126 | - irsasetups/status 127 | verbs: 128 | - get 129 | - patch 130 | - update 131 | - apiGroups: 132 | - rbac.authorization.k8s.io 133 | resources: 134 | - clusterrolebindings 135 | verbs: 136 | - create 137 | - delete 138 | - get 139 | - list 140 | - patch 141 | - update 142 | - watch 143 | - apiGroups: 144 | - rbac.authorization.k8s.io 145 | resources: 146 | - clusterroles 147 | verbs: 148 | - create 149 | - delete 150 | - get 151 | - list 152 | - patch 153 | - update 154 | - watch 155 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: irsa-manager 9 | app.kubernetes.io/part-of: irsa-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: irsa-manager 9 | app.kubernetes.io/part-of: irsa-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/release/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../default 5 | -------------------------------------------------------------------------------- /config/samples/irsa_v1alpha1_irsa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa-manager.kkb0318.github.io/v1alpha1 2 | kind: IRSA 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: irsa 6 | app.kubernetes.io/instance: irsa-sample 7 | app.kubernetes.io/part-of: irsa-manager 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: irsa-manager 10 | name: irsa-sample 11 | spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /config/samples/irsa_v1alpha1_irsasetup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa-manager.kkb0318.github.io/v1alpha1 2 | kind: IRSASetup 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: irsasetup 6 | app.kubernetes.io/instance: irsasetup-sample 7 | app.kubernetes.io/part-of: irsa-manager 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: irsa-manager 10 | name: irsasetup-sample 11 | spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - irsa_v1alpha1_irsasetup.yaml 4 | - irsa_v1alpha1_irsa.yaml 5 | #+kubebuilder:scaffold:manifestskustomizesamples 6 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.34.1 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.34.1 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.34.1 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.34.1 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.34.1 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.34.1 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /cr.yaml: -------------------------------------------------------------------------------- 1 | owner: kkb0318 2 | git-repo: irsa-manager 3 | release-name-template: "helm-{{ .Name }}-{{ .Version }}" 4 | make-release-latest: false 5 | -------------------------------------------------------------------------------- /docs/IRSA-cr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkb0318/irsa-manager/f1775f8f3c5292227af701ecf7e48e15e50229d3/docs/IRSA-cr.png -------------------------------------------------------------------------------- /docs/IRSASetup-cr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkb0318/irsa-manager/f1775f8f3c5292227af701ecf7e48e15e50229d3/docs/IRSASetup-cr.png -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## Packages 4 | - [irsa-manager.kkb0318.github.io/v1alpha1](#irsa-managerkkb0318githubiov1alpha1) 5 | 6 | 7 | ## irsa-manager.kkb0318.github.io/v1alpha1 8 | 9 | Package v1alpha1 contains API Schema definitions for the irsa v1alpha1 API group 10 | 11 | ### Resource Types 12 | - [IRSA](#irsa) 13 | - [IRSASetup](#irsasetup) 14 | 15 | 16 | 17 | #### Discovery 18 | 19 | 20 | 21 | Discovery holds the configuration for IdP Discovery, which is crucial for locating 22 | the OIDC provider in a self-hosted environment. 23 | 24 | 25 | 26 | _Appears in:_ 27 | - [IRSASetupSpec](#irsasetupspec) 28 | 29 | | Field | Description | Default | Validation | 30 | | --- | --- | --- | --- | 31 | | `s3` _[S3Discovery](#s3discovery)_ | S3 specifies the AWS S3 bucket details where the OIDC provider's discovery information is hosted. | | | 32 | 33 | 34 | 35 | 36 | #### IRSA 37 | 38 | 39 | 40 | IRSA is the Schema for the irsas API 41 | 42 | 43 | 44 | 45 | 46 | | Field | Description | Default | Validation | 47 | | --- | --- | --- | --- | 48 | | `apiVersion` _string_ | `irsa-manager.kkb0318.github.io/v1alpha1` | | | 49 | | `kind` _string_ | `IRSA` | | | 50 | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | 51 | | `spec` _[IRSASpec](#irsaspec)_ | | | | 52 | 53 | 54 | 55 | 56 | 57 | 58 | #### IRSAServiceAccount 59 | 60 | 61 | 62 | IRSAServiceAccount represents the details of the Kubernetes service account 63 | 64 | 65 | 66 | _Appears in:_ 67 | - [IRSASpec](#irsaspec) 68 | 69 | | Field | Description | Default | Validation | 70 | | --- | --- | --- | --- | 71 | | `name` _string_ | Name represents the name of the Kubernetes service account | | | 72 | | `namespaces` _string array_ | Namespaces represents the list of namespaces where the service account is used | | | 73 | 74 | 75 | #### IRSASetup 76 | 77 | 78 | 79 | IRSASetup represents a configuration for setting up IAM Roles for Service Accounts (IRSA) in a Kubernetes cluster. 80 | 81 | 82 | 83 | 84 | 85 | | Field | Description | Default | Validation | 86 | | --- | --- | --- | --- | 87 | | `apiVersion` _string_ | `irsa-manager.kkb0318.github.io/v1alpha1` | | | 88 | | `kind` _string_ | `IRSASetup` | | | 89 | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | 90 | | `spec` _[IRSASetupSpec](#irsasetupspec)_ | | | | 91 | 92 | 93 | #### IRSASetupSpec 94 | 95 | 96 | 97 | IRSASetupSpec defines the desired state of IRSASetup 98 | 99 | 100 | 101 | _Appears in:_ 102 | - [IRSASetup](#irsasetup) 103 | 104 | | Field | Description | Default | Validation | 105 | | --- | --- | --- | --- | 106 | | `cleanup` _boolean_ | Cleanup, when enabled, allows the IRSASetup to perform garbage collection
of resources that are no longer needed or managed. | | | 107 | | `mode` _[SetupMode](#setupmode)_ | Mode specifies the operation mode of the controller.
Possible values:
- "selfhosted": For self-managed Kubernetes clusters.
- "eks": For Amazon EKS environments.
Default: "selfhosted" | | Enum: [selfhosted eks]
| 108 | | `discovery` _[Discovery](#discovery)_ | Discovery configures the IdP Discovery process, essential for setting up IRSA by locating
the OIDC provider information.
Only applicable when Mode is "selfhosted". | | | 109 | | `iamOIDCProvider` _string_ | IamOIDCProvider configures IAM OIDC IamOIDCProvider Name
Only applicable when Mode is "eks". | | | 110 | 111 | 112 | 113 | 114 | #### IRSASpec 115 | 116 | 117 | 118 | IRSASpec defines the desired state of IRSA 119 | 120 | 121 | 122 | _Appears in:_ 123 | - [IRSA](#irsa) 124 | 125 | | Field | Description | Default | Validation | 126 | | --- | --- | --- | --- | 127 | | `cleanup` _boolean_ | Cleanup, when enabled, allows the IRSA to perform garbage collection
of resources that are no longer needed or managed. | | | 128 | | `serviceAccount` _[IRSAServiceAccount](#irsaserviceaccount)_ | ServiceAccount represents the Kubernetes service account associated with the IRSA. | | | 129 | | `iamRole` _[IamRole](#iamrole)_ | IamRole represents the IAM role details associated with the IRSA. | | | 130 | | `iamPolicies` _string array_ | IamPolicies represents the list of IAM policies to be attached to the IAM role.
You can set both the policy name (only AWS default policies) or the full ARN. | | | 131 | 132 | 133 | 134 | 135 | #### IamRole 136 | 137 | 138 | 139 | IamRole represents the IAM role configuration 140 | 141 | 142 | 143 | _Appears in:_ 144 | - [IRSASpec](#irsaspec) 145 | 146 | | Field | Description | Default | Validation | 147 | | --- | --- | --- | --- | 148 | | `name` _string_ | Name represents the name of the IAM role. | | | 149 | 150 | 151 | #### S3Discovery 152 | 153 | 154 | 155 | S3Discovery contains the specifics of the S3 bucket used for hosting OIDC provider discovery information. 156 | 157 | 158 | 159 | _Appears in:_ 160 | - [Discovery](#discovery) 161 | 162 | | Field | Description | Default | Validation | 163 | | --- | --- | --- | --- | 164 | | `region` _string_ | Region denotes the AWS region where the S3 bucket is located. | | | 165 | | `bucketName` _string_ | BucketName is the name of the S3 bucket that hosts the OIDC discovery information. | | | 166 | 167 | 168 | 169 | 170 | #### SetupMode 171 | 172 | _Underlying type:_ _string_ 173 | 174 | 175 | 176 | _Validation:_ 177 | - Enum: [selfhosted eks] 178 | 179 | _Appears in:_ 180 | - [IRSASetupSpec](#irsasetupspec) 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /docs/config.yaml: -------------------------------------------------------------------------------- 1 | processor: 2 | ignoreTypes: 3 | - "List$" 4 | - "CommonStatus" 5 | ignoreFields: 6 | - "status$" 7 | - "TypeMeta$" 8 | 9 | render: 10 | kubernetesVersion: 1.29 11 | -------------------------------------------------------------------------------- /docs/eks-setup.md: -------------------------------------------------------------------------------- 1 | ## Setup for EKS 2 | 3 | Define and apply an IRSASetup custom resource. 4 | 5 | ```yaml 6 | apiVersion: irsa-manager.kkb0318.github.io/v1alpha1 7 | kind: IRSASetup 8 | metadata: 9 | name: irsa-init 10 | namespace: irsa-manager-system 11 | spec: 12 | mode: eks 13 | cleanup: false 14 | iamOIDCProvider: "oidc.eks..amazonaws.com/id/" 15 | ``` 16 | 17 | Check the IRSASetup custom resource status to verify whether it is set to true. 18 | -------------------------------------------------------------------------------- /docs/irsa-manager-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkb0318/irsa-manager/f1775f8f3c5292227af701ecf7e48e15e50229d3/docs/irsa-manager-overview.png -------------------------------------------------------------------------------- /docs/selfhosted-setup.md: -------------------------------------------------------------------------------- 1 | ## Setup for Self-Hosted 2 | 3 | ![](./IRSASetup-cr.png) 4 | 5 | ### Define and apply an IRSASetup custom resource according to your needs. 6 | 7 | ```yaml 8 | apiVersion: irsa-manager.kkb0318.github.io/v1alpha1 9 | kind: IRSASetup 10 | metadata: 11 | name: irsa-init 12 | namespace: irsa-manager-system 13 | spec: 14 | cleanup: false 15 | mode: selfhosted 16 | discovery: 17 | s3: 18 | region: 19 | bucketName: 20 | ``` 21 | 22 | Check the IRSASetup custom resource status to verify whether it is set to true. 23 | 24 | > [!NOTE] 25 | > Please ensure that only one IRSASetup resource is created. 26 | 27 | ### Modify kube-apiserver Settings 28 | 29 | If the IRSASetup status is true, a key file (Name: `irsa-manager-key` , Namespace: `kube-system` ) will be created. This is used for signing tokens in the kubernetes API. 30 | Execute the following commands on the control plane server to save the public and private keys locally for Kubernetes signatures: 31 | 32 | ```console 33 | kubectl get secret -n kube-system irsa-manager-key -o jsonpath="{.data.ssh-privatekey}" | base64 --decode | sudo tee /path/to/file.key > /dev/null 34 | kubectl get secret -n kube-system irsa-manager-key -o jsonpath="{.data.ssh-publickey}" | base64 --decode | sudo tee /path/to/file.pub > /dev/null 35 | ``` 36 | 37 | > [!NOTE] 38 | > Path: `/path/to/file` can be any path you choose. 39 | > If you use kubeadm, it is recommended to set `/etc/kubernetes/pki/irsa-manager.(key|pub)` 40 | 41 | Then, modify the kube-apiserver settings to include the following parameters: 42 | 43 | - API Audiences 44 | 45 | ``` 46 | --api-audiences=sts.amazonaws.com,https://kubernetes.default.svc.cluster.local 47 | ``` 48 | 49 | - Service Account Issuer 50 | 51 | ``` 52 | --service-account-issuer=https://s3-.amazonaws.com/ 53 | ``` 54 | 55 | > [!NOTE] 56 | > Add this setting as the first element. 57 | > When this flag is specified multiple times, the first is used to generate tokens and all are used to determine which issuers are accepted. 58 | 59 | - Service Account Key File 60 | 61 | The public key generated previously can be read by the API server. Add the path for this parameter flag: 62 | 63 | ``` 64 | --service-account-key-file=/path/to/file.pub 65 | ``` 66 | 67 | > [!NOTE] 68 | > If you do not mount /path/to directory, you need to add the volumes field to this path. 69 | 70 | - Service Account Signing Key File 71 | 72 | The private key (oidc-issuer.key) generated previously can be read by the API server. Add the path for this parameter flag: 73 | 74 | ``` 75 | --service-account-signing-key-file=/path/to/file.key 76 | ``` 77 | 78 | > [!NOTE] 79 | > Overwrite the existing settings. 80 | > If you do not mount /path/to directory, you need to add the volumes field to this path. 81 | 82 | For more details, refer to the [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#serviceaccount-token-volume-projection). 83 | 84 | Example configuration: 85 | ``` 86 | ... 87 | - --service-account-issuer=https://s3-.amazonaws.com/ 88 | - --service-account-issuer=https://kubernetes.default.svc.cluster.local 89 | - --service-account-key-file=/etc/kubernetes/pki/irsa-manager.pub 90 | - --service-account-key-file=/etc/kubernetes/pki/sa.pub 91 | - --service-account-signing-key-file=/etc/kubernetes/pki/irsa-manager.key 92 | - --service-cluster-ip-range=10.96.0.0/16 93 | - --tls-cert-file=/etc/kubernetes/pki/apiserver.crt 94 | - --tls-private-key-file=/etc/kubernetes/pki/apiserver.key 95 | - --api-audiences=sts.amazonaws.com,https://kubernetes.default.svc.cluster.local 96 | ... 97 | ``` 98 | -------------------------------------------------------------------------------- /examples/eks.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa-manager.kkb0318.github.io/v1alpha1 2 | kind: IRSASetup 3 | metadata: 4 | name: irsa-init 5 | namespace: irsa-manager-system 6 | spec: 7 | mode: eks 8 | cleanup: true 9 | iamOIDCProvider: "oidc.eks..amazonaws.com/id/" 10 | -------------------------------------------------------------------------------- /examples/irsa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa-manager.kkb0318.github.io/v1alpha1 2 | kind: IRSA 3 | metadata: 4 | name: irsa-sample 5 | namespace: irsa-manager-system 6 | spec: 7 | cleanup: true 8 | serviceAccount: 9 | name: irsa111-sa 10 | namespaces: 11 | - kube-system 12 | - default 13 | iamRole: 14 | name: irsa111-role 15 | iamPolicies: 16 | - AmazonS3FullAccess 17 | -------------------------------------------------------------------------------- /examples/selfhosted.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: irsa-manager.kkb0318.github.io/v1alpha1 2 | kind: IRSASetup 3 | metadata: 4 | name: irsa-init 5 | namespace: irsa-manager-system 6 | spec: 7 | cleanup: false 8 | discovery: 9 | s3: 10 | region: ap-northeast-1 11 | bucketName: irsa-manager-test-f4vhae 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kkb0318/irsa-manager 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.2 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go-v2 v1.30.3 9 | github.com/aws/aws-sdk-go-v2/config v1.27.27 10 | github.com/aws/aws-sdk-go-v2/credentials v1.17.27 11 | github.com/aws/aws-sdk-go-v2/service/iam v1.34.3 12 | github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 13 | github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 14 | github.com/aws/smithy-go v1.20.3 15 | github.com/go-jose/go-jose/v4 v4.0.4 16 | github.com/onsi/ginkgo/v2 v2.19.0 17 | github.com/onsi/gomega v1.33.1 18 | github.com/stretchr/testify v1.9.0 19 | k8s.io/api v0.30.3 20 | k8s.io/apimachinery v0.30.3 21 | k8s.io/client-go v0.30.3 22 | sigs.k8s.io/controller-runtime v0.18.4 23 | ) 24 | 25 | require ( 26 | github.com/fatih/color v1.10.0 // indirect 27 | github.com/mattn/go-colorable v0.1.8 // indirect 28 | github.com/mattn/go-isatty v0.0.12 // indirect 29 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 30 | ) 31 | 32 | require ( 33 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect 34 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect 35 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect 36 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect 37 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 38 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect 40 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect 41 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect 42 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect 43 | github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect 44 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect 45 | github.com/beorn7/perks v1.0.1 // indirect 46 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 47 | github.com/davecgh/go-spew v1.1.1 // indirect 48 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 49 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 50 | github.com/fsnotify/fsnotify v1.7.0 // indirect 51 | github.com/go-logr/logr v1.4.2 // indirect 52 | github.com/go-logr/zapr v1.3.0 // indirect 53 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 54 | github.com/go-openapi/jsonreference v0.21.0 // indirect 55 | github.com/go-openapi/swag v0.23.0 // indirect 56 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 57 | github.com/goccy/go-yaml v1.11.3 58 | github.com/gogo/protobuf v1.3.2 // indirect 59 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 60 | github.com/golang/protobuf v1.5.4 // indirect 61 | github.com/google/gnostic-models v0.6.8 // indirect 62 | github.com/google/go-cmp v0.6.0 // indirect 63 | github.com/google/gofuzz v1.2.0 // indirect 64 | github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect 65 | github.com/google/uuid v1.6.0 // indirect 66 | github.com/imdario/mergo v0.3.16 // indirect 67 | github.com/josharian/intern v1.0.0 // indirect 68 | github.com/json-iterator/go v1.1.12 // indirect 69 | github.com/mailru/easyjson v0.7.7 // indirect 70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 71 | github.com/modern-go/reflect2 v1.0.2 // indirect 72 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 73 | github.com/pkg/errors v0.9.1 // indirect 74 | github.com/pmezard/go-difflib v1.0.0 // indirect 75 | github.com/prometheus/client_golang v1.19.1 // indirect 76 | github.com/prometheus/client_model v0.6.1 // indirect 77 | github.com/prometheus/common v0.55.0 // indirect 78 | github.com/prometheus/procfs v0.15.1 // indirect 79 | github.com/spf13/pflag v1.0.5 // indirect 80 | go.uber.org/multierr v1.11.0 // indirect 81 | go.uber.org/zap v1.27.0 // indirect 82 | golang.org/x/crypto v0.25.0 // indirect 83 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 84 | golang.org/x/net v0.27.0 // indirect 85 | golang.org/x/oauth2 v0.21.0 // indirect 86 | golang.org/x/sys v0.22.0 // indirect 87 | golang.org/x/term v0.22.0 // indirect 88 | golang.org/x/text v0.16.0 // indirect 89 | golang.org/x/time v0.5.0 // indirect 90 | golang.org/x/tools v0.23.0 // indirect 91 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 92 | google.golang.org/protobuf v1.34.2 // indirect 93 | gopkg.in/inf.v0 v0.9.1 // indirect 94 | gopkg.in/yaml.v2 v2.4.0 // indirect 95 | gopkg.in/yaml.v3 v3.0.1 // indirect 96 | k8s.io/apiextensions-apiserver v0.30.3 // indirect 97 | k8s.io/klog/v2 v2.130.1 // indirect 98 | k8s.io/kube-openapi v0.0.0-20240730131305-7a9a4e85957e // indirect 99 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect 100 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 101 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 102 | sigs.k8s.io/yaml v1.4.0 // indirect 103 | ) 104 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /internal/aws/aws_role.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "slices" 10 | "strings" 11 | 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | "github.com/aws/aws-sdk-go-v2/service/iam" 14 | "github.com/aws/aws-sdk-go-v2/service/iam/types" 15 | "github.com/aws/smithy-go" 16 | irsav1alpha1 "github.com/kkb0318/irsa-manager/api/v1alpha1" 17 | "github.com/kkb0318/irsa-manager/internal/issuer" 18 | ) 19 | 20 | // RoleManager represents the details needed to manage IAM roles 21 | type RoleManager struct { 22 | // RoleName represents the name of the IAM role 23 | RoleName string 24 | // ServiceAccount represents the ServiceAccount Name and namespaces associated with the role 25 | ServiceAccount irsav1alpha1.IRSAServiceAccount 26 | // Policies represents the list of policies to be attached to the role 27 | Policies []string 28 | 29 | // AccountId represents the AWS Account Id 30 | AccountId string 31 | } 32 | 33 | // PolicyArn returns the full ARN of a given policy name. If the policy name already has the full ARN, it returns it as is. 34 | func (r *RoleManager) PolicyArn(policy string) *string { 35 | prefix := "arn:aws:iam::" 36 | if strings.HasPrefix(policy, prefix) { 37 | return aws.String(policy) 38 | } 39 | return aws.String(fmt.Sprintf("%saws:policy/%s", prefix, policy)) 40 | } 41 | 42 | // ExtractNewPolicies returns the names of the policies that are in the current settings (r.Policies) but are not yet attached to the role. 43 | func (r *RoleManager) ExtractNewPolicies(l *iam.ListAttachedRolePoliciesOutput) []string { 44 | result := []string{} 45 | if l == nil { 46 | return r.Policies // return all policies 47 | } 48 | for _, p := range r.Policies { 49 | if !slices.ContainsFunc(l.AttachedPolicies, func(ap types.AttachedPolicy) bool { 50 | return *r.PolicyArn(p) == *ap.PolicyArn 51 | }) { 52 | result = append(result, p) 53 | } 54 | } 55 | return result 56 | } 57 | 58 | // ExtractStalePolicies returns the ARNs of the policies that are attached to the role but are not in the current settings (r.Policies). 59 | func (r *RoleManager) ExtractStalePolicies(l *iam.ListAttachedRolePoliciesOutput) []string { 60 | result := []string{} 61 | if l == nil { 62 | return result 63 | } 64 | for _, ap := range l.AttachedPolicies { 65 | if !slices.ContainsFunc(r.Policies, func(p string) bool { 66 | return *r.PolicyArn(p) == *ap.PolicyArn 67 | }) { 68 | result = append(result, *ap.PolicyArn) 69 | } 70 | } 71 | return result 72 | } 73 | 74 | // DeleteIRSARole detaches specified policies from the IAM role and deletes the IAM role 75 | func (a *AwsIamClient) DeleteIRSARole(ctx context.Context, r RoleManager) error { 76 | for _, policy := range r.Policies { 77 | err := a.DetachRolePolicy(ctx, aws.String(r.RoleName), r.PolicyArn(policy)) 78 | if err != nil { 79 | return err 80 | } 81 | log.Printf("Policy %s detached from role %s successfully", policy, r.RoleName) 82 | } 83 | input := &iam.DeleteRoleInput{RoleName: aws.String(r.RoleName)} 84 | _, err := a.Client.DeleteRole(ctx, input) 85 | // Ignore error if the role does not exist or there are other policies that this controller does not manage 86 | if errorHandler(err, []string{"DeleteConflict", "NoSuchEntity"}) != nil { 87 | return err 88 | } 89 | log.Printf("Role %s deleted successfully", r.RoleName) 90 | return nil 91 | } 92 | 93 | // DetachRolePolicy detaches specified policies from the IAM role 94 | func (a *AwsIamClient) DetachRolePolicy(ctx context.Context, roleName, policyArn *string) error { 95 | detachRolePolicyInput := &iam.DetachRolePolicyInput{ 96 | RoleName: roleName, 97 | PolicyArn: policyArn, 98 | } 99 | _, err := a.Client.DetachRolePolicy(ctx, detachRolePolicyInput) 100 | // Ignore error if the policy is already detached or the role does not exist 101 | if errorHandler(err, []string{"NoSuchEntity"}) != nil { 102 | return err 103 | } 104 | return nil 105 | } 106 | 107 | // AttachRolePolicy attaches specidied policy 108 | func (a *AwsIamClient) AttachRolePolicy(ctx context.Context, roleName, policyArn *string) error { 109 | attachRolePolicyInput := &iam.AttachRolePolicyInput{ 110 | RoleName: roleName, 111 | PolicyArn: policyArn, 112 | } 113 | _, err := a.Client.AttachRolePolicy(ctx, attachRolePolicyInput) 114 | return err 115 | } 116 | 117 | // UpdateIRSARole creates an IAM role with the specified trust policy and attaches specified policies to it 118 | func (a *AwsIamClient) UpdateIRSARole(ctx context.Context, issuerMeta issuer.OIDCIssuerMeta, r RoleManager) error { 119 | providerArn := fmt.Sprintf("arn:aws:iam::%s:oidc-provider/%s", r.AccountId, issuerMeta.IssuerHostPath()) 120 | statement := make([]map[string]interface{}, len(r.ServiceAccount.Namespaces)) 121 | for i, ns := range r.ServiceAccount.Namespaces { 122 | statement[i] = map[string]interface{}{ 123 | "Effect": "Allow", 124 | "Principal": map[string]interface{}{ 125 | "Federated": providerArn, 126 | }, 127 | "Action": "sts:AssumeRoleWithWebIdentity", 128 | "Condition": map[string]interface{}{ 129 | "StringEquals": map[string]interface{}{ 130 | fmt.Sprintf("%s:sub", issuerMeta.IssuerHostPath()): fmt.Sprintf("system:serviceaccount:%s:%s", ns, r.ServiceAccount.Name), 131 | }, 132 | }, 133 | } 134 | } 135 | trustPolicy := map[string]interface{}{ 136 | "Version": "2012-10-17", 137 | "Statement": statement, 138 | } 139 | trustPolicyJSON, err := json.Marshal(trustPolicy) 140 | if err != nil { 141 | return fmt.Errorf("failed to marshal trust policy: %w", err) 142 | } 143 | createRoleInput := &iam.CreateRoleInput{ 144 | RoleName: aws.String(r.RoleName), 145 | AssumeRolePolicyDocument: aws.String(string(trustPolicyJSON)), 146 | } 147 | 148 | _, err = a.Client.CreateRole(ctx, createRoleInput) 149 | if errorHandler(err, []string{"EntityAlreadyExists"}) != nil { 150 | return err 151 | } 152 | log.Printf("Role %s created successfully", r.RoleName) 153 | 154 | updateRoleInput := &iam.UpdateAssumeRolePolicyInput{ 155 | RoleName: aws.String(r.RoleName), 156 | PolicyDocument: aws.String(string(trustPolicyJSON)), 157 | } 158 | 159 | _, err = a.Client.UpdateAssumeRolePolicy(ctx, updateRoleInput) 160 | if err != nil { 161 | return fmt.Errorf("failed to update assume role policy for role %s: %w", r.RoleName, err) 162 | } 163 | 164 | listPoliciesOutput, err := a.Client.ListAttachedRolePolicies(ctx, &iam.ListAttachedRolePoliciesInput{RoleName: aws.String(r.RoleName)}) 165 | if err != nil { 166 | return fmt.Errorf("failed to list attached role policies with %s: %w", r.RoleName, err) 167 | } 168 | 169 | for _, policy := range r.ExtractNewPolicies(listPoliciesOutput) { 170 | err := a.AttachRolePolicy(ctx, aws.String(r.RoleName), r.PolicyArn(policy)) 171 | if err != nil { 172 | return err 173 | } 174 | log.Printf("Policy %s attached to role %s successfully", policy, r.RoleName) 175 | } 176 | for _, policy := range r.ExtractStalePolicies(listPoliciesOutput) { 177 | err := a.DetachRolePolicy(ctx, aws.String(r.RoleName), r.PolicyArn(policy)) 178 | if err != nil { 179 | return err 180 | } 181 | log.Printf("Policy %s detached to role %s successfully", policy, r.RoleName) 182 | } 183 | log.Printf("Assume role policy for %s updated successfully", r.RoleName) 184 | return nil 185 | } 186 | 187 | // errorHandler handles specific errors by checking the error code against a list of codes to ignore 188 | func errorHandler(err error, errorCodes []string) error { 189 | if err != nil { 190 | var ae smithy.APIError 191 | if errors.As(err, &ae) && slices.Contains(errorCodes, ae.ErrorCode()) { 192 | // fmt.Printf("Skipped error: %s \n", err.Error()) 193 | return nil 194 | } 195 | } 196 | return err 197 | } 198 | -------------------------------------------------------------------------------- /internal/aws/aws_role_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/service/iam" 8 | "github.com/aws/aws-sdk-go-v2/service/iam/types" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestExtractNewPolicies(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | policies []string 16 | attachedPolicies *iam.ListAttachedRolePoliciesOutput 17 | expected []string 18 | }{ 19 | { 20 | "PolicyAlreadyAttached", 21 | []string{"ReadOnlyAccess", "AdministratorAccess"}, 22 | &iam.ListAttachedRolePoliciesOutput{ 23 | AttachedPolicies: []types.AttachedPolicy{ 24 | {PolicyArn: aws.String("arn:aws:iam::aws:policy/ReadOnlyAccess")}, 25 | }, 26 | }, 27 | []string{"AdministratorAccess"}, 28 | }, 29 | { 30 | "NoPolicyAttached", 31 | []string{"PowerUserAccess", "SecurityAudit"}, 32 | &iam.ListAttachedRolePoliciesOutput{ 33 | AttachedPolicies: []types.AttachedPolicy{ 34 | {PolicyArn: aws.String("arn:aws:iam::aws:policy/ReadOnlyAccess")}, 35 | }, 36 | }, 37 | []string{"PowerUserAccess", "SecurityAudit"}, 38 | }, 39 | { 40 | "AllPoliciesAlreadyAttached", 41 | []string{"ReadOnlyAccess"}, 42 | &iam.ListAttachedRolePoliciesOutput{ 43 | AttachedPolicies: []types.AttachedPolicy{ 44 | {PolicyArn: aws.String("arn:aws:iam::aws:policy/ReadOnlyAccess")}, 45 | }, 46 | }, 47 | []string{}, 48 | }, 49 | } 50 | 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | r := &RoleManager{Policies: tt.policies} 54 | result := r.ExtractNewPolicies(tt.attachedPolicies) 55 | assert.Equal(t, tt.expected, result) 56 | }) 57 | } 58 | } 59 | 60 | func TestExtractStalePolicies(t *testing.T) { 61 | tests := []struct { 62 | name string 63 | policies []string 64 | attachedPolicies *iam.ListAttachedRolePoliciesOutput 65 | expected []string 66 | }{ 67 | { 68 | "StalePolicyExists", 69 | []string{"ReadOnlyAccess", "AdministratorAccess"}, 70 | &iam.ListAttachedRolePoliciesOutput{ 71 | AttachedPolicies: []types.AttachedPolicy{ 72 | {PolicyArn: aws.String("arn:aws:iam::aws:policy/ReadOnlyAccess")}, 73 | {PolicyArn: aws.String("arn:aws:iam::aws:policy/AdministratorAccess")}, 74 | {PolicyArn: aws.String("arn:aws:iam::aws:policy/PowerUserAccess")}, 75 | }, 76 | }, 77 | []string{"arn:aws:iam::aws:policy/PowerUserAccess"}, 78 | }, 79 | { 80 | "MultipleStalePoliciesExist", 81 | []string{"ReadOnlyAccess", "SecurityAudit"}, 82 | &iam.ListAttachedRolePoliciesOutput{ 83 | AttachedPolicies: []types.AttachedPolicy{ 84 | {PolicyArn: aws.String("arn:aws:iam::aws:policy/PowerUserAccess")}, 85 | {PolicyArn: aws.String("arn:aws:iam::aws:policy/AdministratorAccess")}, 86 | }, 87 | }, 88 | []string{"arn:aws:iam::aws:policy/PowerUserAccess", "arn:aws:iam::aws:policy/AdministratorAccess"}, 89 | }, 90 | { 91 | "NoStalePolicies", 92 | []string{"ReadOnlyAccess"}, 93 | &iam.ListAttachedRolePoliciesOutput{ 94 | AttachedPolicies: []types.AttachedPolicy{ 95 | {PolicyArn: aws.String("arn:aws:iam::aws:policy/ReadOnlyAccess")}, 96 | }, 97 | }, 98 | []string{}, 99 | }, 100 | } 101 | 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | r := &RoleManager{Policies: tt.policies} 105 | result := r.ExtractStalePolicies(tt.attachedPolicies) 106 | assert.Equal(t, tt.expected, result) 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /internal/aws/client.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/config" 10 | "github.com/aws/aws-sdk-go-v2/credentials/stscreds" 11 | "github.com/aws/aws-sdk-go-v2/service/iam" 12 | "github.com/aws/aws-sdk-go-v2/service/s3" 13 | "github.com/aws/aws-sdk-go-v2/service/sts" 14 | ) 15 | 16 | type AwsClient interface { 17 | IamClient() *AwsIamClient 18 | StsClient() *AwsStsClient 19 | S3Client(region, bucketName string) *AwsS3Client 20 | } 21 | 22 | type AwsIamClient struct { 23 | Client AwsIamAPI 24 | } 25 | type AwsStsClient struct { 26 | Client AwsStsAPI 27 | } 28 | type AwsS3Client struct { 29 | Client AwsS3API 30 | region string 31 | bucketName string 32 | } 33 | 34 | func NewAwsClientFactory(ctx context.Context) (*AwsClientFactory, error) { 35 | cfg, err := config.LoadDefaultConfig( 36 | ctx, 37 | ) 38 | if err != nil { 39 | return nil, fmt.Errorf("unable to load SDK config, %w", err) 40 | } 41 | roleArn := os.Getenv("AWS_ROLE_ARN") 42 | if roleArn != "" { 43 | stsSvc := sts.NewFromConfig(cfg) 44 | creds := stscreds.NewAssumeRoleProvider(stsSvc, roleArn) 45 | cfg.Credentials = aws.NewCredentialsCache(creds) 46 | } 47 | return &AwsClientFactory{config: cfg}, nil 48 | } 49 | 50 | func (a *AwsClientFactory) IamClient() *AwsIamClient { 51 | return &AwsIamClient{ 52 | iam.NewFromConfig(a.config), 53 | } 54 | } 55 | 56 | func (a *AwsClientFactory) StsClient() *AwsStsClient { 57 | return &AwsStsClient{ 58 | sts.NewFromConfig(a.config), 59 | } 60 | } 61 | 62 | func (a *AwsClientFactory) S3Client(region, bucketName string) *AwsS3Client { 63 | return &AwsS3Client{ 64 | Client: s3.NewFromConfig(a.config), 65 | region: region, 66 | bucketName: bucketName, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/controller/irsa_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | awsclient "github.com/kkb0318/irsa-manager/internal/aws" 24 | "github.com/kkb0318/irsa-manager/internal/handler" 25 | "github.com/kkb0318/irsa-manager/internal/issuer" 26 | "github.com/kkb0318/irsa-manager/internal/kubernetes" 27 | "github.com/kkb0318/irsa-manager/internal/manifests" 28 | "github.com/kkb0318/irsa-manager/internal/utils" 29 | "k8s.io/apimachinery/pkg/runtime" 30 | "k8s.io/apimachinery/pkg/types" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 34 | ctrllog "sigs.k8s.io/controller-runtime/pkg/log" 35 | 36 | irsav1alpha1 "github.com/kkb0318/irsa-manager/api/v1alpha1" 37 | ) 38 | 39 | // IRSAReconciler reconciles a IRSA object 40 | type IRSAReconciler struct { 41 | client.Client 42 | Scheme *runtime.Scheme 43 | AwsClient awsclient.AwsClient 44 | } 45 | 46 | //+kubebuilder:rbac:groups=irsa-manager.kkb0318.github.io,resources=irsas,verbs=get;list;watch;create;update;patch;delete 47 | //+kubebuilder:rbac:groups=irsa-manager.kkb0318.github.io,resources=irsas/status,verbs=get;update;patch 48 | //+kubebuilder:rbac:groups=irsa-manager.kkb0318.github.io,resources=irsas/finalizers,verbs=update 49 | //+kubebuilder:rbac:groups=irsa-manager.kkb0318.github.io,resources=irsasetups,verbs=get;list 50 | //+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete 51 | 52 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 53 | // move the current state of the cluster closer to the desired state. 54 | func (r *IRSAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 55 | log := ctrllog.FromContext(ctx) 56 | obj := &irsav1alpha1.IRSA{} 57 | if err := r.Get(ctx, req.NamespacedName, obj); err != nil { 58 | return ctrl.Result{}, client.IgnoreNotFound(err) 59 | } 60 | if r.AwsClient == nil { 61 | awsClient, err := awsclient.NewAwsClientFactory(ctx) 62 | if err != nil { 63 | return ctrl.Result{}, err 64 | } 65 | r.AwsClient = awsClient 66 | } 67 | kubeClient, err := kubernetes.NewKubernetesClient(r.Client, kubernetes.Owner{Field: "irsa-manager"}) 68 | if err != nil { 69 | return ctrl.Result{}, err 70 | } 71 | if !controllerutil.ContainsFinalizer(obj, irsamanagerFinalizer) { 72 | controllerutil.AddFinalizer(obj, irsamanagerFinalizer) 73 | if err := r.Update(ctx, obj); err != nil { 74 | log.Error(err, "Failed to update custom resource to add finalizer") 75 | return ctrl.Result{}, err 76 | } 77 | return ctrl.Result{Requeue: true}, nil 78 | } 79 | 80 | defer func() { 81 | if err := r.Get(ctx, req.NamespacedName, &irsav1alpha1.IRSA{}); err != nil { 82 | return 83 | } 84 | statusHandler := handler.NewStatusHandler(kubeClient) 85 | if err := statusHandler.Patch(ctx, obj); err != nil { 86 | return 87 | } 88 | }() 89 | 90 | if !obj.DeletionTimestamp.IsZero() { 91 | err = r.reconcileDelete(ctx, obj, kubeClient) 92 | if err != nil { 93 | return ctrl.Result{}, err 94 | } 95 | controllerutil.RemoveFinalizer(obj, irsamanagerFinalizer) 96 | err = r.Update(ctx, obj) 97 | if err == nil { 98 | log.Info("successfully deleted") 99 | } 100 | return ctrl.Result{}, err 101 | } 102 | 103 | if err := r.reconcile(ctx, obj, kubeClient); err != nil { 104 | return ctrl.Result{}, err 105 | } 106 | 107 | log.Info("successfully reconciled") 108 | return ctrl.Result{}, nil 109 | } 110 | 111 | func (r *IRSAReconciler) reconcileDelete(ctx context.Context, obj *irsav1alpha1.IRSA, kubeClient *kubernetes.KubernetesClient) error { 112 | if !obj.Spec.Cleanup { 113 | return nil 114 | } 115 | roleManager := awsclient.RoleManager{ 116 | RoleName: obj.Spec.IamRole.Name, 117 | Policies: obj.Spec.IamPolicies, 118 | } 119 | err := r.AwsClient.IamClient().DeleteIRSARole( 120 | ctx, 121 | roleManager, 122 | ) 123 | if err != nil { 124 | return err 125 | } 126 | deleted, err := cleanupKubernetesResources(ctx, kubeClient, obj.Spec.ServiceAccount.NamespacedNameList()) 127 | *obj = irsav1alpha1.IRSAStatusSetServiceAccount(*obj, deleted) 128 | if err != nil { 129 | return err 130 | } 131 | return nil 132 | } 133 | 134 | func (r *IRSAReconciler) reconcile(ctx context.Context, obj *irsav1alpha1.IRSA, kubeClient *kubernetes.KubernetesClient) error { 135 | list, err := kubeClient.List(ctx, irsav1alpha1.GroupVersion.WithKind(irsav1alpha1.IRSASetupKind)) 136 | if err != nil { 137 | return err 138 | } 139 | if len(list.Items) != 1 { 140 | return fmt.Errorf("there should be exactly one IRSASetup item") 141 | } 142 | irsaSetup := &irsav1alpha1.IRSASetup{} 143 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.Items[0].Object, irsaSetup) 144 | if err != nil { 145 | return fmt.Errorf("error converting to IRSASetup for %s: %v", list.Items[0].GetName(), err) 146 | } 147 | serviceAccount := obj.Spec.ServiceAccount 148 | issuerMeta, err := issuer.NewOIDCIssuerMeta(irsaSetup) 149 | if err != nil { 150 | return err 151 | } 152 | // e is set only when an error occurs in an external dependency process and is reflected in the CRs status 153 | var e error 154 | var reason irsav1alpha1.IRSAReason 155 | defer func() { 156 | if e != nil { 157 | *obj = irsav1alpha1.IRSAStatusNotReady(*obj, string(reason), e.Error()) 158 | } 159 | }() 160 | 161 | accountId, err := r.AwsClient.StsClient().GetAccountId() 162 | if err != nil { 163 | e = err 164 | return err 165 | } 166 | roleManager := awsclient.RoleManager{ 167 | RoleName: obj.Spec.IamRole.Name, 168 | ServiceAccount: serviceAccount, 169 | Policies: obj.Spec.IamPolicies, 170 | AccountId: accountId, 171 | } 172 | err = r.AwsClient.IamClient().UpdateIRSARole( 173 | ctx, 174 | issuerMeta, 175 | roleManager, 176 | ) 177 | if err != nil { 178 | e = err 179 | reason = irsav1alpha1.IRSAReasonFailedRoleUpdate 180 | return err 181 | } 182 | 183 | kubeHandler := handler.NewKubernetesHandler(kubeClient) 184 | for _, namespacedName := range serviceAccount.NamespacedNameList() { 185 | sa := manifests.NewServiceAccountBuilder().WithIRSAAnnotation(roleManager).Build(namespacedName) 186 | kubeHandler.Append(sa) 187 | } 188 | applied, err := kubeHandler.ApplyAll(ctx) 189 | *obj = irsav1alpha1.IRSAStatusSetServiceAccount(*obj, applied) 190 | if err != nil { 191 | e = err 192 | reason = irsav1alpha1.IRSAReasonFailedK8sApply 193 | return err 194 | } 195 | 196 | deleted, err := cleanupKubernetesResources( 197 | ctx, 198 | kubeClient, 199 | utils.DiffNamespacedNames(obj.Status.ServiceNamespacedNameList(), serviceAccount.NamespacedNameList()), 200 | ) 201 | *obj = irsav1alpha1.IRSAStatusRemoveServiceAccount(*obj, deleted) 202 | if err != nil { 203 | e = err 204 | reason = irsav1alpha1.IRSAReasonFailedK8sCleanUp 205 | return err 206 | } 207 | *obj = irsav1alpha1.IRSAStatusReady(*obj, string(irsav1alpha1.IRSAReasonReady), "successfully setup resources") 208 | return nil 209 | } 210 | 211 | func cleanupKubernetesResources(ctx context.Context, client *kubernetes.KubernetesClient, nsNames []types.NamespacedName) ([]types.NamespacedName, error) { 212 | kubeHandler := handler.NewKubernetesHandler(client) 213 | for _, namespacedName := range nsNames { 214 | sa := manifests.NewServiceAccountBuilder().Build(namespacedName) 215 | kubeHandler.Append(sa) 216 | } 217 | deleted, err := kubeHandler.DeleteAll(ctx) 218 | if err != nil { 219 | return deleted, err 220 | } 221 | return deleted, nil 222 | } 223 | 224 | // SetupWithManager sets up the controller with the Manager. 225 | func (r *IRSAReconciler) SetupWithManager(mgr ctrl.Manager) error { 226 | return ctrl.NewControllerManagedBy(mgr). 227 | For(&irsav1alpha1.IRSA{}). 228 | Complete(r) 229 | } 230 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "fmt" 21 | "path/filepath" 22 | "runtime" 23 | "testing" 24 | "time" 25 | 26 | . "github.com/onsi/ginkgo/v2" 27 | . "github.com/onsi/gomega" 28 | 29 | regv1 "k8s.io/api/admissionregistration/v1" 30 | appsv1 "k8s.io/api/apps/v1" 31 | corev1 "k8s.io/api/core/v1" 32 | rbacv1 "k8s.io/api/rbac/v1" 33 | "k8s.io/apimachinery/pkg/types" 34 | "k8s.io/client-go/kubernetes/scheme" 35 | "k8s.io/client-go/rest" 36 | ctrl "sigs.k8s.io/controller-runtime" 37 | "sigs.k8s.io/controller-runtime/pkg/client" 38 | "sigs.k8s.io/controller-runtime/pkg/envtest" 39 | logf "sigs.k8s.io/controller-runtime/pkg/log" 40 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 41 | 42 | irsav1alpha1 "github.com/kkb0318/irsa-manager/api/v1alpha1" 43 | //+kubebuilder:scaffold:imports 44 | ) 45 | 46 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 47 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 48 | 49 | var ( 50 | cfg *rest.Config 51 | k8sClient client.Client 52 | testEnv *envtest.Environment 53 | timeout = time.Second * 10 54 | ctx = ctrl.SetupSignalHandler() 55 | ) 56 | 57 | func TestControllers(t *testing.T) { 58 | RegisterFailHandler(Fail) 59 | 60 | RunSpecs(t, "Controller Suite") 61 | } 62 | 63 | var _ = BeforeSuite(func() { 64 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 65 | 66 | By("bootstrapping test environment") 67 | testEnv = &envtest.Environment{ 68 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 69 | ErrorIfCRDPathMissing: true, 70 | 71 | // The BinaryAssetsDirectory is only required if you want to run the tests directly 72 | // without call the makefile target test. If not informed it will look for the 73 | // default path defined in controller-runtime which is /usr/local/kubebuilder/. 74 | // Note that you must have the required binaries setup under the bin directory to perform 75 | // the tests directly. When we run make test it will be setup and used automatically. 76 | BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", 77 | fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), 78 | } 79 | 80 | var err error 81 | // cfg is defined in this file globally. 82 | cfg, err = testEnv.Start() 83 | Expect(err).NotTo(HaveOccurred()) 84 | Expect(cfg).NotTo(BeNil()) 85 | 86 | err = irsav1alpha1.AddToScheme(scheme.Scheme) 87 | Expect(err).NotTo(HaveOccurred()) 88 | 89 | //+kubebuilder:scaffold:scheme 90 | 91 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 92 | Expect(err).NotTo(HaveOccurred()) 93 | Expect(k8sClient).NotTo(BeNil()) 94 | }) 95 | 96 | var _ = AfterSuite(func() { 97 | By("tearing down the test environment") 98 | err := testEnv.Stop() 99 | Expect(err).NotTo(HaveOccurred()) 100 | }) 101 | 102 | type expectedResource struct { 103 | types.NamespacedName 104 | f func() client.Object 105 | } 106 | 107 | func checkExist(resource expectedResource) { 108 | Eventually(func() error { 109 | found := resource.f() 110 | return k8sClient.Get(ctx, resource.NamespacedName, found) 111 | }, timeout).Should(Succeed()) 112 | } 113 | 114 | func checkNoExist(resource expectedResource) { 115 | Eventually(func() error { 116 | found := resource.f() 117 | return k8sClient.Get(ctx, resource.NamespacedName, found) 118 | }, timeout).Should(Not(Succeed())) 119 | } 120 | 121 | func newSecret() client.Object { 122 | return &corev1.Secret{} 123 | } 124 | 125 | func newMutatingWebhookConfiguration() client.Object { 126 | return ®v1.MutatingWebhookConfiguration{} 127 | } 128 | 129 | func newService() client.Object { 130 | return &corev1.Service{} 131 | } 132 | 133 | func newDeployment() client.Object { 134 | return &appsv1.Deployment{} 135 | } 136 | 137 | func newServiceAccount() client.Object { 138 | return &corev1.ServiceAccount{} 139 | } 140 | 141 | func newClusterRole() client.Object { 142 | return &rbacv1.ClusterRole{} 143 | } 144 | 145 | func newClusterRoleBinding() client.Object { 146 | return &rbacv1.ClusterRoleBinding{} 147 | } 148 | -------------------------------------------------------------------------------- /internal/eks/validation.go: -------------------------------------------------------------------------------- 1 | package eks 2 | 3 | import ( 4 | "fmt" 5 | 6 | irsav1alpha1 "github.com/kkb0318/irsa-manager/api/v1alpha1" 7 | ) 8 | 9 | func Validate(obj *irsav1alpha1.IRSASetup) error { 10 | if obj.Spec.IamOIDCProvider == "" { 11 | return fmt.Errorf("IamOIDCProvider parameter must be set when Mode is 'eks'") 12 | } 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /internal/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | type KubernetesClient interface { 12 | Apply(ctx context.Context, obj client.Object) error 13 | Create(ctx context.Context, obj client.Object) error 14 | Get(ctx context.Context, obj client.Object) (*unstructured.Unstructured, error) 15 | Delete(ctx context.Context, obj client.Object, opts DeleteOptions) error 16 | } 17 | 18 | type StatusClient interface { 19 | PatchStatus(ctx context.Context, obj client.Object) error 20 | } 21 | 22 | // DeleteOptions contains options for delete requests. 23 | type DeleteOptions struct { 24 | // DeletionPropagation decides how the garbage collector will handle the propagation. 25 | DeletionPropagation metav1.DeletionPropagation 26 | 27 | // Inclusions determines which in-cluster objects are subject to deletion 28 | // based on the labels. 29 | // A nil Inclusions map means all objects are subject to deletion 30 | Inclusions map[string]string 31 | } 32 | -------------------------------------------------------------------------------- /internal/handler/kubernetes.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "k8s.io/apimachinery/pkg/api/errors" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/types" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | type KubernetesHandler struct { 14 | client KubernetesClient 15 | objs []client.Object 16 | } 17 | 18 | func NewKubernetesHandler(c KubernetesClient) *KubernetesHandler { 19 | return &KubernetesHandler{ 20 | client: c, 21 | objs: []client.Object{}, 22 | } 23 | } 24 | 25 | func (k *KubernetesHandler) Append(obj client.Object) { 26 | k.objs = append(k.objs, obj) 27 | } 28 | 29 | // CreateAll creates the given objects (AlreadyExists errors are ignored) 30 | func (k *KubernetesHandler) CreateAll(ctx context.Context) error { 31 | for _, obj := range k.objs { 32 | err := k.client.Create(ctx, obj) 33 | if err != nil { 34 | if !errors.IsAlreadyExists(err) { 35 | return err 36 | } 37 | log.Printf("resource %s/%s already exists. skipped to create \n", obj.GetNamespace(), obj.GetName()) 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func (k *KubernetesHandler) ApplyAll(ctx context.Context) ([]types.NamespacedName, error) { 44 | applied := []types.NamespacedName{} 45 | for _, obj := range k.objs { 46 | err := k.client.Apply(ctx, obj) 47 | if err != nil { 48 | return applied, err 49 | } 50 | applied = append(applied, client.ObjectKeyFromObject(obj)) 51 | } 52 | return applied, nil 53 | } 54 | 55 | func (k *KubernetesHandler) DeleteAll(ctx context.Context) ([]types.NamespacedName, error) { 56 | deleted := []types.NamespacedName{} 57 | for _, obj := range k.objs { 58 | err := k.client.Delete(ctx, obj, DeleteOptions{ 59 | DeletionPropagation: metav1.DeletePropagationBackground, 60 | }) 61 | if err != nil { 62 | return deleted, err 63 | } 64 | deleted = append(deleted, client.ObjectKeyFromObject(obj)) 65 | } 66 | return deleted, nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/handler/status.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type StatusHandler struct { 10 | client StatusClient 11 | } 12 | 13 | func NewStatusHandler(c StatusClient) *StatusHandler { 14 | return &StatusHandler{ 15 | client: c, 16 | } 17 | } 18 | 19 | func (s *StatusHandler) Patch(ctx context.Context, obj client.Object) error { 20 | return s.client.PatchStatus(ctx, obj) 21 | } 22 | -------------------------------------------------------------------------------- /internal/issuer/issuer.go: -------------------------------------------------------------------------------- 1 | package issuer 2 | 3 | import ( 4 | "fmt" 5 | 6 | irsav1alpha1 "github.com/kkb0318/irsa-manager/api/v1alpha1" 7 | ) 8 | 9 | type OIDCIssuerMeta interface { 10 | IssuerHostPath() string 11 | IssuerUrl() string 12 | } 13 | 14 | type s3IssuerMeta struct { 15 | region string 16 | bucketName string 17 | } 18 | 19 | func NewOIDCIssuerMeta(i *irsav1alpha1.IRSASetup) (OIDCIssuerMeta, error) { 20 | if i.Spec.Mode == irsav1alpha1.ModeEks { 21 | return newIamOIDCProviderIssuerMeta(i.Spec.IamOIDCProvider) 22 | } 23 | return newS3IssuerMeta(&i.Spec.Discovery.S3) 24 | } 25 | 26 | func newS3IssuerMeta(s3 *irsav1alpha1.S3Discovery) (*s3IssuerMeta, error) { 27 | region := s3.Region 28 | bucketName := s3.BucketName 29 | if region == "" || bucketName == "" { 30 | return nil, fmt.Errorf("s3 region and bucket name must not be empty. region: %s, bucketName: %s", region, bucketName) 31 | } 32 | return &s3IssuerMeta{region, bucketName}, nil 33 | } 34 | 35 | func (i *s3IssuerMeta) IssuerHostPath() string { 36 | return fmt.Sprintf("s3-%s.amazonaws.com/%s", i.region, i.bucketName) 37 | } 38 | 39 | // IssuerUrl constructs the URL path for the OIDC issuer based on the provided AWS region and bucket name. 40 | // This utility function generates the expected host path for accessing the OIDC configuration stored in an S3 bucket. 41 | func (i *s3IssuerMeta) IssuerUrl() string { 42 | return fmt.Sprintf("https://%s", i.IssuerHostPath()) 43 | } 44 | 45 | func newIamOIDCProviderIssuerMeta(providerName string) (*iamOIDCProviderIssuerMeta, error) { 46 | if providerName == "" { 47 | return nil, fmt.Errorf("IAM OIDC Provider Name must not be empty") 48 | } 49 | return &iamOIDCProviderIssuerMeta{providerName}, nil 50 | } 51 | 52 | type iamOIDCProviderIssuerMeta struct { 53 | providerName string 54 | } 55 | 56 | func (i *iamOIDCProviderIssuerMeta) IssuerHostPath() string { 57 | return i.providerName 58 | } 59 | 60 | func (i *iamOIDCProviderIssuerMeta) IssuerUrl() string { 61 | return fmt.Sprintf("https://%s", i.IssuerHostPath()) 62 | } 63 | -------------------------------------------------------------------------------- /internal/kubernetes/apply.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | func (c KubernetesClient) Apply(ctx context.Context, obj client.Object) error { 10 | opts := []client.PatchOption{ 11 | client.ForceOwnership, 12 | client.FieldOwner(c.owner.Field), 13 | } 14 | u, err := c.toUnstructured(obj) 15 | if err != nil { 16 | return err 17 | } 18 | err = c.client.Patch(ctx, u, client.Apply, opts...) 19 | if err != nil { 20 | return err 21 | } 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/kubernetes/client.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/client" 5 | ) 6 | 7 | type KubernetesClient struct { 8 | client client.Client 9 | owner Owner 10 | } 11 | 12 | func NewKubernetesClient(c client.Client, owner Owner) (*KubernetesClient, error) { 13 | return &KubernetesClient{ 14 | client: c, 15 | owner: owner, 16 | }, nil 17 | } 18 | -------------------------------------------------------------------------------- /internal/kubernetes/create.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | func (c KubernetesClient) Create(ctx context.Context, obj client.Object) error { 10 | opts := []client.CreateOption{ 11 | client.FieldOwner(c.owner.Field), 12 | } 13 | u, err := c.toUnstructured(obj) 14 | if err != nil { 15 | return err 16 | } 17 | err = c.client.Create(ctx, u, opts...) 18 | if err != nil { 19 | return err 20 | } 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/kubernetes/delete.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | 9 | "github.com/kkb0318/irsa-manager/internal/handler" 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/labels" 12 | 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | // Delete deletes the given object (not found errors are ignored). 17 | func (c *KubernetesClient) Delete(ctx context.Context, obj client.Object, opts handler.DeleteOptions) error { 18 | existingObj, err := c.Get(ctx, obj) 19 | if err != nil { 20 | if !errors.IsNotFound(err) { 21 | return fmt.Errorf("failed to delete: %w", err) 22 | } 23 | return nil // already deleted 24 | } 25 | 26 | sel, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: opts.Inclusions}) 27 | if err != nil { 28 | return fmt.Errorf("label selector failed: %w", err) 29 | } 30 | 31 | if !sel.Matches(labels.Set(existingObj.GetLabels())) { 32 | return nil 33 | } 34 | 35 | if err := c.client.Delete(ctx, existingObj, client.PropagationPolicy(opts.DeletionPropagation)); err != nil { 36 | return fmt.Errorf("delete failed: %w", err) 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/kubernetes/get.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | // Get gets the given object. 11 | func (c KubernetesClient) Get(ctx context.Context, obj client.Object) (*unstructured.Unstructured, error) { 12 | u, err := c.toUnstructured(obj) 13 | if err != nil { 14 | return nil, err 15 | } 16 | existingObj := &unstructured.Unstructured{} 17 | existingObj.SetGroupVersionKind(u.GroupVersionKind()) 18 | err = c.client.Get(ctx, client.ObjectKeyFromObject(u), existingObj) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return existingObj, nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/kubernetes/list.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | // List lists all resources of the given kind. 12 | func (c KubernetesClient) List(ctx context.Context, gvk schema.GroupVersionKind, listOpts ...client.ListOption) (*unstructured.UnstructuredList, error) { 13 | list := &unstructured.UnstructuredList{} 14 | list.SetGroupVersionKind(gvk) 15 | err := c.client.List(ctx, list, listOpts...) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return list, nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/kubernetes/owner.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | // Owner contains options for setting the field manager. 4 | type Owner struct { 5 | // Field sets the field manager name for the given server-side apply patch. 6 | Field string 7 | } 8 | -------------------------------------------------------------------------------- /internal/kubernetes/status.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | func (c KubernetesClient) PatchStatus(ctx context.Context, obj client.Object) error { 10 | opts := &client.SubResourcePatchOptions{ 11 | PatchOptions: client.PatchOptions{ 12 | FieldManager: c.owner.Field, 13 | }, 14 | } 15 | u, err := c.toUnstructured(obj) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | return c.client.Status().Patch(ctx, u, client.Apply, opts) 21 | } 22 | -------------------------------------------------------------------------------- /internal/kubernetes/unstructured.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 9 | ) 10 | 11 | func (c KubernetesClient) toUnstructured(obj client.Object) (*unstructured.Unstructured, error) { 12 | gvk, err := apiutil.GVKForObject(obj, c.client.Scheme()) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | u := &unstructured.Unstructured{} 18 | unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 19 | if err != nil { 20 | return nil, err 21 | } 22 | u.Object = unstructured 23 | u.SetGroupVersionKind(gvk) 24 | u.SetManagedFields(nil) 25 | return u, nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/manifests/secret.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "github.com/kkb0318/irsa-manager/internal/selfhosted" 5 | corev1 "k8s.io/api/core/v1" 6 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/types" 8 | ) 9 | 10 | func SshKeyNamespacedName() types.NamespacedName { 11 | return types.NamespacedName{ 12 | Namespace: "kube-system", 13 | Name: "irsa-manager-key", 14 | } 15 | } 16 | 17 | type SecretBuilder struct { 18 | data map[string][]byte 19 | secretType corev1.SecretType 20 | } 21 | 22 | type TlsCredential interface { 23 | Certificate() []byte 24 | PrivateKey() []byte 25 | } 26 | 27 | func NewSecretBuilder() *SecretBuilder { 28 | return &SecretBuilder{ 29 | secretType: corev1.SecretTypeOpaque, 30 | } 31 | } 32 | 33 | func (b *SecretBuilder) WithSSHKey(keyPair selfhosted.KeyPair) *SecretBuilder { 34 | b.data = map[string][]byte{ 35 | "ssh-publickey": keyPair.PublicKey(), 36 | corev1.SSHAuthPrivateKey: keyPair.PrivateKey(), 37 | } 38 | b.secretType = corev1.SecretTypeSSHAuth 39 | return b 40 | } 41 | 42 | func (b *SecretBuilder) WithCertificate(t TlsCredential) *SecretBuilder { 43 | b.data = map[string][]byte{ 44 | "tls.crt": t.Certificate(), 45 | "tls.key": t.PrivateKey(), 46 | } 47 | b.secretType = corev1.SecretTypeTLS 48 | return b 49 | } 50 | 51 | func (b *SecretBuilder) Build(namespacedName types.NamespacedName) (*corev1.Secret, error) { 52 | secret := &corev1.Secret{ 53 | ObjectMeta: v1.ObjectMeta{ 54 | Name: namespacedName.Name, 55 | Namespace: namespacedName.Namespace, 56 | }, 57 | TypeMeta: v1.TypeMeta{ 58 | APIVersion: corev1.SchemeGroupVersion.String(), 59 | Kind: "Secret", 60 | }, 61 | Type: b.secretType, 62 | Data: b.data, 63 | } 64 | return secret, nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/manifests/serviceaccount.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "fmt" 5 | 6 | awsclient "github.com/kkb0318/irsa-manager/internal/aws" 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/types" 10 | ) 11 | 12 | type ServiceAccountBuilder struct { 13 | annotation map[string]string 14 | } 15 | 16 | func NewServiceAccountBuilder() *ServiceAccountBuilder { 17 | return &ServiceAccountBuilder{} 18 | } 19 | 20 | func (b *ServiceAccountBuilder) WithIRSAAnnotation(role awsclient.RoleManager) *ServiceAccountBuilder { 21 | b.annotation = map[string]string{ 22 | "eks.amazonaws.com/role-arn": fmt.Sprintf("arn:aws:iam::%s:role/%s", role.AccountId, role.RoleName), 23 | } 24 | return b 25 | } 26 | 27 | func (b *ServiceAccountBuilder) Build(namespacedName types.NamespacedName) *corev1.ServiceAccount { 28 | return &corev1.ServiceAccount{ 29 | TypeMeta: metav1.TypeMeta{ 30 | APIVersion: corev1.SchemeGroupVersion.String(), 31 | Kind: "ServiceAccount", 32 | }, 33 | ObjectMeta: metav1.ObjectMeta{ 34 | Name: namespacedName.Name, 35 | Namespace: namespacedName.Namespace, 36 | Annotations: b.annotation, 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/selfhosted/certificate.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | -------------------------------------------------------------------------------- /internal/selfhosted/jwks.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "errors" 9 | "fmt" 10 | 11 | jose "github.com/go-jose/go-jose/v4" 12 | "k8s.io/client-go/util/keyutil" 13 | ) 14 | 15 | // keyIDFromPublicKey derives a key ID non-reversibly from a public key. 16 | // 17 | // The Key ID is field on a given on JWTs and JWKs that help relying parties 18 | // pick the correct key for verification when the identity party advertises 19 | // multiple keys. 20 | // 21 | // Making the derivation non-reversible makes it impossible for someone to 22 | // accidentally obtain the real key from the key ID and use it for token 23 | // validation. 24 | // This method is copied from 25 | // https://github.com/kubernetes/kubernetes/blob/v1.29.3/pkg/serviceaccount/jwt.go#L99 26 | func keyIDFromPublicKey(publicKey interface{}) (string, error) { 27 | publicKeyDERBytes, err := x509.MarshalPKIXPublicKey(publicKey) 28 | if err != nil { 29 | return "", fmt.Errorf("failed to serialize public key to DER format: %v", err) 30 | } 31 | 32 | hasher := crypto.SHA256.New() 33 | hasher.Write(publicKeyDERBytes) 34 | publicKeyDERHash := hasher.Sum(nil) 35 | 36 | keyID := base64.RawURLEncoding.EncodeToString(publicKeyDERHash) 37 | 38 | return keyID, nil 39 | } 40 | 41 | type JWK struct { 42 | Keys []jose.JSONWebKey `json:"keys"` 43 | } 44 | 45 | func NewJWK(pub []byte) (*JWK, error) { 46 | pubKeys, err := keyutil.ParsePublicKeysPEM(pub) 47 | if err != nil { 48 | return nil, err 49 | } 50 | pubKey := pubKeys[0] 51 | var alg jose.SignatureAlgorithm 52 | switch pubKey.(type) { 53 | case *rsa.PublicKey: 54 | alg = jose.RS256 55 | default: 56 | return nil, errors.New("public key is not RSA") 57 | } 58 | 59 | kid, err := keyIDFromPublicKey(pubKey) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | var keys []jose.JSONWebKey 65 | keys = append(keys, jose.JSONWebKey{ 66 | Key: pubKey, 67 | KeyID: kid, 68 | Algorithm: string(alg), 69 | Use: "sig", 70 | }) 71 | keys = append(keys, jose.JSONWebKey{ 72 | Key: pubKey, 73 | KeyID: "", 74 | Algorithm: string(alg), 75 | Use: "sig", 76 | }) 77 | return &JWK{Keys: keys}, nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/selfhosted/jwks_test.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | const rsaKeyID = "JHJehTTTZlsspKHT-GaJxK7Kd1NQgZJu3fyK6K_QDYU" 11 | 12 | func TestJWK(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | filename string 16 | expected string 17 | expectErr bool 18 | }{ 19 | { 20 | name: "rsa", 21 | filename: "testdata/rsa.pub", 22 | expected: rsaKeyID, 23 | }, 24 | { 25 | name: "no rsa", 26 | filename: "testdata/ecdsa.pub", 27 | expectErr: true, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | content, err := os.ReadFile(tt.filename) 33 | assert.NoError(t, err) 34 | actual, err := NewJWK(content) 35 | if tt.expectErr { 36 | assert.Error(t, err, "") 37 | } else { 38 | assert.NoError(t, err) 39 | assert.Equal(t, tt.expected, actual.Keys[0].KeyID) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/selfhosted/keys.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/pem" 8 | ) 9 | 10 | type KeyPair struct { 11 | publicKey []byte 12 | privateKey []byte 13 | } 14 | 15 | func CreateKeyPair() (*KeyPair, error) { 16 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | // convert private key to PEM 22 | privPem := pem.Block{ 23 | Type: "RSA PRIVATE KEY", 24 | Bytes: x509.MarshalPKCS1PrivateKey(privateKey), 25 | } 26 | privPemBytes := pem.EncodeToMemory(&privPem) 27 | 28 | // convert public key to PKIX, ASN.1 DER 29 | pubASN1, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | pubPem := pem.Block{ 35 | Type: "PUBLIC KEY", 36 | Bytes: pubASN1, 37 | } 38 | pubPemBytes := pem.EncodeToMemory(&pubPem) 39 | return &KeyPair{pubPemBytes, privPemBytes}, nil 40 | } 41 | 42 | func (k *KeyPair) PublicKey() []byte { 43 | return k.publicKey 44 | } 45 | 46 | func (k *KeyPair) PrivateKey() []byte { 47 | return k.privateKey 48 | } 49 | -------------------------------------------------------------------------------- /internal/selfhosted/keys_test.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha256" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestReadKey(t *testing.T) { 16 | t.Run("key pair check", func(t *testing.T) { 17 | keyPair, err := CreateKeyPair() 18 | assert.NoError(t, err) 19 | 20 | message := []byte("test message") 21 | hashed := sha256.Sum256(message) 22 | 23 | block, _ := pem.Decode(keyPair.PrivateKey()) 24 | assert.NotNil(t, block, "failed to decode private key to PEM") 25 | 26 | privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) 27 | assert.NoError(t, err, "failed to parse private key") 28 | 29 | signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:]) 30 | 31 | assert.NoError(t, err, "failed to create signature") 32 | block, _ = pem.Decode(keyPair.PublicKey()) 33 | assert.NotNil(t, block, "failed to decode public key to PEM") 34 | 35 | pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) 36 | assert.NoError(t, err, "failed to parse public key") 37 | 38 | rsaPubKey, ok := pubKey.(*rsa.PublicKey) 39 | assert.Truef(t, ok, "public key is not RSA") 40 | 41 | err = rsa.VerifyPKCS1v15(rsaPubKey, crypto.SHA256, hashed[:], signature) 42 | assert.NoError(t, err, "failed to check signature") 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /internal/selfhosted/oidc.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kkb0318/irsa-manager/internal/issuer" 7 | ) 8 | 9 | type OIDCIdP interface { 10 | Create(ctx context.Context) error 11 | IsUpdate() (bool, error) 12 | Update(ctx context.Context) error 13 | Delete(ctx context.Context) error 14 | } 15 | 16 | type OIDCIdPDiscoveryContents interface { 17 | Discovery() ([]byte, error) 18 | JWK() ([]byte, error) 19 | JWKsFileName() string 20 | } 21 | 22 | type OIDCIdPDiscovery interface { 23 | CreateStorage(ctx context.Context) error 24 | Upload(ctx context.Context, o OIDCIdPDiscoveryContents, forceUpdate bool) error 25 | Delete(ctx context.Context, o OIDCIdPDiscoveryContents) error 26 | } 27 | 28 | type OIDCIdPFactory interface { 29 | IdP(i issuer.OIDCIssuerMeta) (OIDCIdP, error) 30 | IdPDiscovery() OIDCIdPDiscovery 31 | IdPDiscoveryContents(i issuer.OIDCIssuerMeta) OIDCIdPDiscoveryContents 32 | } 33 | -------------------------------------------------------------------------------- /internal/selfhosted/oidc/factory.go: -------------------------------------------------------------------------------- 1 | package oidc 2 | 3 | import ( 4 | "context" 5 | 6 | awsclient "github.com/kkb0318/irsa-manager/internal/aws" 7 | "github.com/kkb0318/irsa-manager/internal/issuer" 8 | "github.com/kkb0318/irsa-manager/internal/selfhosted" 9 | ) 10 | 11 | type AwsS3IdPFactory struct { 12 | region string 13 | bucketName string 14 | awsClient awsclient.AwsClient 15 | jwk *selfhosted.JWK 16 | jwksFileName string 17 | } 18 | 19 | func NewAwsS3IdpFactory( 20 | ctx context.Context, 21 | region, 22 | bucketName string, 23 | jwk *selfhosted.JWK, 24 | jwksFileName string, 25 | awsClient awsclient.AwsClient, 26 | ) (*AwsS3IdPFactory, error) { 27 | return &AwsS3IdPFactory{ 28 | region: region, 29 | bucketName: bucketName, 30 | awsClient: awsClient, 31 | jwk: jwk, 32 | jwksFileName: jwksFileName, 33 | }, nil 34 | } 35 | 36 | func (f *AwsS3IdPFactory) IdP(i issuer.OIDCIssuerMeta) (selfhosted.OIDCIdP, error) { 37 | return NewAwsIdP(f.awsClient, i) 38 | } 39 | 40 | func (f *AwsS3IdPFactory) IdPDiscovery() selfhosted.OIDCIdPDiscovery { 41 | return NewS3IdPDiscovery(f.awsClient, f.region, f.bucketName) 42 | } 43 | 44 | func (f *AwsS3IdPFactory) IdPDiscoveryContents(i issuer.OIDCIssuerMeta) selfhosted.OIDCIdPDiscoveryContents { 45 | return NewIdPDiscoveryContents(f.jwk, i, f.jwksFileName) 46 | } 47 | -------------------------------------------------------------------------------- /internal/selfhosted/oidc/id_provider.go: -------------------------------------------------------------------------------- 1 | package oidc 2 | 3 | import ( 4 | "context" 5 | 6 | awsclient "github.com/kkb0318/irsa-manager/internal/aws" 7 | "github.com/kkb0318/irsa-manager/internal/issuer" 8 | ) 9 | 10 | type AwsIdP struct { 11 | iamClient *awsclient.AwsIamClient 12 | stsClient *awsclient.AwsStsClient 13 | issuerMeta issuer.OIDCIssuerMeta 14 | } 15 | 16 | func NewAwsIdP(awsConfig awsclient.AwsClient, issuerMeta issuer.OIDCIssuerMeta) (*AwsIdP, error) { 17 | iamClient := awsConfig.IamClient() 18 | stsClient := awsConfig.StsClient() 19 | return &AwsIdP{iamClient, stsClient, issuerMeta}, nil 20 | } 21 | 22 | func (a *AwsIdP) Create(ctx context.Context) error { 23 | err := a.iamClient.CreateOIDCProvider(ctx, a.issuerMeta.IssuerUrl()) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | func (a *AwsIdP) Update(ctx context.Context) error { 31 | return nil 32 | } 33 | 34 | func (a *AwsIdP) IsUpdate() (bool, error) { 35 | return false, nil 36 | } 37 | 38 | func (a *AwsIdP) Delete(ctx context.Context) error { 39 | accountId, err := a.stsClient.GetAccountId() 40 | if err != nil { 41 | return err 42 | } 43 | return a.iamClient.DeleteOIDCProvider(ctx, accountId, a.issuerMeta.IssuerHostPath()) 44 | } 45 | -------------------------------------------------------------------------------- /internal/selfhosted/oidc/id_provider_discovery.go: -------------------------------------------------------------------------------- 1 | package oidc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | awsclient "github.com/kkb0318/irsa-manager/internal/aws" 8 | "github.com/kkb0318/irsa-manager/internal/selfhosted" 9 | ) 10 | 11 | const CONFIGURATION_PATH = ".well-known/openid-configuration" 12 | 13 | type S3IdPDiscovery struct { 14 | s3Client *awsclient.AwsS3Client 15 | } 16 | 17 | // NewS3IdPDiscovery initializes a new instance of S3IdPCreator with the specified AWS region and bucket name. 18 | // This function attempts to create an AWS client configured for the specified region. 19 | func NewS3IdPDiscovery(awsConfig awsclient.AwsClient, region, bucketName string) *S3IdPDiscovery { 20 | s3Client := awsConfig.S3Client(region, bucketName) 21 | return &S3IdPDiscovery{s3Client} 22 | } 23 | 24 | // CreateStorage creates an S3 bucket 25 | func (s *S3IdPDiscovery) CreateStorage(ctx context.Context) error { 26 | err := s.s3Client.CreateBucketPublic(ctx) 27 | if err != nil { 28 | return fmt.Errorf("unable to create bucket, %w", err) 29 | } 30 | return nil 31 | } 32 | 33 | // Upload uploads the OIDC provider's discovery configuration and JSON Web Key Set (JWKS) to the specified AWS S3 bucket. 34 | // This method is responsible for uploading the necessary OIDC configuration files to S3, making them accessible for OIDC clients. 35 | func (s *S3IdPDiscovery) Upload(ctx context.Context, o selfhosted.OIDCIdPDiscoveryContents, forceUpdate bool) error { 36 | discovery, err := o.Discovery() 37 | if err != nil { 38 | return nil 39 | } 40 | jwk, err := o.JWK() 41 | if err != nil { 42 | return nil 43 | } 44 | inputs := []awsclient.ObjectInput{ 45 | { 46 | Key: CONFIGURATION_PATH, 47 | Body: discovery, 48 | }, 49 | { 50 | Key: o.JWKsFileName(), 51 | Body: jwk, 52 | }, 53 | } 54 | if forceUpdate { 55 | err = s.s3Client.PutObjectsPublic(ctx, inputs) 56 | } else { 57 | err = s.s3Client.CreateObjectsPublic(ctx, inputs) 58 | } 59 | if err != nil { 60 | return fmt.Errorf("unable to upload object, %w", err) 61 | } 62 | return nil 63 | } 64 | 65 | // Delete delete an S3 bucket and objects 66 | func (s *S3IdPDiscovery) Delete(ctx context.Context, o selfhosted.OIDCIdPDiscoveryContents) error { 67 | err := s.s3Client.DeleteObjects(ctx, []string{ 68 | CONFIGURATION_PATH, 69 | o.JWKsFileName(), 70 | }) 71 | if err != nil { 72 | return err 73 | } 74 | err = s.s3Client.DeleteBucket(ctx) 75 | if err != nil { 76 | return err 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /internal/selfhosted/oidc/id_provider_discovery_contents.go: -------------------------------------------------------------------------------- 1 | package oidc 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/kkb0318/irsa-manager/internal/issuer" 8 | "github.com/kkb0318/irsa-manager/internal/selfhosted" 9 | ) 10 | 11 | type IdPDiscoveryContents struct { 12 | jwk *selfhosted.JWK 13 | issuerMeta issuer.OIDCIssuerMeta 14 | jwksFileName string 15 | } 16 | 17 | type oidcDiscoveryConfiguration struct { 18 | Issuer string `json:"issuer"` 19 | JWKSURI string `json:"jwks_uri"` 20 | AuthorizationEndpoint string `json:"authorization_endpoint"` 21 | ResponseTypesSupported []string `json:"response_types_supported"` 22 | SubjectTypesSupported []string `json:"subject_types_supported"` 23 | IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` 24 | ClaimsSupported []string `json:"claims_supported"` 25 | } 26 | 27 | func NewIdPDiscoveryContents(jwk *selfhosted.JWK, issuerMeta issuer.OIDCIssuerMeta, jwksFileName string) *IdPDiscoveryContents { 28 | return &IdPDiscoveryContents{jwk, issuerMeta, jwksFileName} 29 | } 30 | 31 | func (p *IdPDiscoveryContents) Discovery() ([]byte, error) { 32 | oidcConfig := oidcDiscoveryConfiguration{ 33 | Issuer: fmt.Sprintf("%s/", p.issuerMeta.IssuerUrl()), 34 | JWKSURI: fmt.Sprintf("%s/%s", p.issuerMeta.IssuerUrl(), p.jwksFileName), 35 | AuthorizationEndpoint: "urn:kubernetes:programmatic_authorization", 36 | ResponseTypesSupported: []string{"id_token"}, 37 | SubjectTypesSupported: []string{"public"}, 38 | IDTokenSigningAlgValuesSupported: []string{"RS256"}, 39 | ClaimsSupported: []string{"sub", "iss"}, 40 | } 41 | jsonData, err := json.MarshalIndent(oidcConfig, "", " ") 42 | if err != nil { 43 | return nil, err 44 | } 45 | return jsonData, nil 46 | } 47 | 48 | func (p *IdPDiscoveryContents) JWK() ([]byte, error) { 49 | jsonData, err := json.MarshalIndent(p.jwk, "", " ") 50 | if err != nil { 51 | return nil, err 52 | } 53 | return jsonData, nil 54 | } 55 | 56 | func (p *IdPDiscoveryContents) JWKsFileName() string { 57 | return p.jwksFileName 58 | } 59 | -------------------------------------------------------------------------------- /internal/selfhosted/selfhosted.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kkb0318/irsa-manager/internal/issuer" 7 | ) 8 | 9 | func Execute(ctx context.Context, idpComponentsFactory OIDCIdPFactory, issuerMeta issuer.OIDCIssuerMeta, forceUpdate bool) error { 10 | discovery := idpComponentsFactory.IdPDiscovery() 11 | discoveryContents := idpComponentsFactory.IdPDiscoveryContents(issuerMeta) 12 | idp, err := idpComponentsFactory.IdP(issuerMeta) 13 | if err != nil { 14 | return err 15 | } 16 | err = discovery.CreateStorage(ctx) 17 | if err != nil { 18 | return err 19 | } 20 | err = discovery.Upload(ctx, discoveryContents, forceUpdate) 21 | if err != nil { 22 | return err 23 | } 24 | err = idp.Create(ctx) 25 | if err != nil { 26 | return err 27 | } 28 | return nil 29 | } 30 | 31 | func Delete(ctx context.Context, factory OIDCIdPFactory, issuerMeta issuer.OIDCIssuerMeta) error { 32 | discovery := factory.IdPDiscovery() 33 | discoveryContents := factory.IdPDiscoveryContents(issuerMeta) 34 | idp, err := factory.IdP(issuerMeta) 35 | if err != nil { 36 | return err 37 | } 38 | err = discovery.Delete(ctx, discoveryContents) 39 | if err != nil { 40 | return err 41 | } 42 | err = idp.Delete(ctx) 43 | if err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/selfhosted/testdata/ecdsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPL 3 | X2i8uIp/C/ASqiIGUeeKQtX0/IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /internal/selfhosted/testdata/rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249XwEo9k4tM8fMxV7zx 3 | OhcrP+WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecI 4 | zshKuv1gKIxbbLQMOuK1eA/4HALyEkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG 5 | 51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJU 6 | j7OTh/AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEi 7 | B4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRM 8 | WwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/client" 5 | ) 6 | 7 | type Webhook interface { 8 | Resources() []client.Object 9 | } 10 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/base_manifests.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "github.com/kkb0318/irsa-manager/internal/manifests" 5 | regv1 "k8s.io/api/admissionregistration/v1" 6 | appsv1 "k8s.io/api/apps/v1" 7 | corev1 "k8s.io/api/core/v1" 8 | rbacv1 "k8s.io/api/rbac/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/types" 11 | "k8s.io/apimachinery/pkg/util/intstr" 12 | ) 13 | 14 | type baseManifestFactory struct { 15 | deploymentMeta types.NamespacedName 16 | serviceMeta types.NamespacedName 17 | serviceAccountMeta types.NamespacedName 18 | mutatingWebhookConfigurationMeta types.NamespacedName 19 | podLabel map[string]string 20 | } 21 | 22 | func serviceNamespacedName() types.NamespacedName { 23 | return types.NamespacedName{ 24 | Name: "pod-identity-webhook", 25 | Namespace: WEBHOOK_NAMESPACE, 26 | } 27 | } 28 | 29 | const WEBHOOK_NAMESPACE = "kube-system" 30 | 31 | func newBaseManifestFactory() *baseManifestFactory { 32 | return &baseManifestFactory{ 33 | deploymentMeta: types.NamespacedName{ 34 | Name: "pod-identity-webhook", 35 | Namespace: WEBHOOK_NAMESPACE, 36 | }, 37 | serviceMeta: serviceNamespacedName(), 38 | serviceAccountMeta: types.NamespacedName{ 39 | Name: "pod-identity-webhook", 40 | Namespace: WEBHOOK_NAMESPACE, 41 | }, 42 | mutatingWebhookConfigurationMeta: types.NamespacedName{ 43 | Name: "pod-identity-webhook", 44 | Namespace: WEBHOOK_NAMESPACE, 45 | }, 46 | podLabel: map[string]string{"app": "pod-identity-webhook"}, 47 | } 48 | } 49 | 50 | func (b *baseManifestFactory) mutatingWebhookConfiguration() *regv1.MutatingWebhookConfiguration { 51 | path := "/mutate" 52 | failurePolicy := regv1.Ignore 53 | sideEffects := regv1.SideEffectClassNone 54 | return ®v1.MutatingWebhookConfiguration{ 55 | TypeMeta: metav1.TypeMeta{ 56 | APIVersion: regv1.SchemeGroupVersion.String(), 57 | Kind: "MutatingWebhookConfiguration", 58 | }, 59 | ObjectMeta: metav1.ObjectMeta{ 60 | Name: b.mutatingWebhookConfigurationMeta.Name, 61 | Namespace: b.mutatingWebhookConfigurationMeta.Namespace, 62 | }, 63 | Webhooks: []regv1.MutatingWebhook{ 64 | { 65 | Name: "pod-identity-webhook.amazonaws.com", 66 | ClientConfig: regv1.WebhookClientConfig{ 67 | Service: ®v1.ServiceReference{ 68 | Name: b.serviceMeta.Name, 69 | Namespace: b.serviceMeta.Namespace, 70 | Path: &path, 71 | }, 72 | }, 73 | Rules: []regv1.RuleWithOperations{ 74 | { 75 | Operations: []regv1.OperationType{"CREATE"}, 76 | Rule: regv1.Rule{ 77 | APIGroups: []string{""}, 78 | APIVersions: []string{"v1"}, 79 | Resources: []string{"pods"}, 80 | }, 81 | }, 82 | }, 83 | FailurePolicy: &failurePolicy, 84 | SideEffects: &sideEffects, 85 | AdmissionReviewVersions: []string{"v1beta1"}, 86 | }, 87 | }, 88 | } 89 | } 90 | 91 | func (b *baseManifestFactory) deployment() *appsv1.Deployment { 92 | replicas := int32(1) 93 | return &appsv1.Deployment{ 94 | TypeMeta: metav1.TypeMeta{ 95 | APIVersion: appsv1.SchemeGroupVersion.String(), 96 | Kind: "Deployment", 97 | }, 98 | ObjectMeta: metav1.ObjectMeta{ 99 | Name: b.deploymentMeta.Name, 100 | Namespace: b.deploymentMeta.Namespace, 101 | }, 102 | Spec: appsv1.DeploymentSpec{ 103 | Replicas: &replicas, 104 | Selector: &metav1.LabelSelector{ 105 | MatchLabels: b.podLabel, 106 | }, 107 | Template: corev1.PodTemplateSpec{ 108 | ObjectMeta: metav1.ObjectMeta{ 109 | Labels: b.podLabel, 110 | }, 111 | Spec: corev1.PodSpec{ 112 | ServiceAccountName: b.serviceAccountMeta.Name, 113 | Containers: []corev1.Container{ 114 | { 115 | Name: "pod-identity-webhook", 116 | Image: "amazon/amazon-eks-pod-identity-webhook:latest", 117 | ImagePullPolicy: corev1.PullAlways, 118 | // Command: []string{}, // Command must be patched 119 | VolumeMounts: []corev1.VolumeMount{ 120 | { 121 | Name: "cert", 122 | MountPath: "/etc/webhook/certs", 123 | ReadOnly: true, 124 | }, 125 | }, 126 | }, 127 | }, 128 | // Volumes: []corev1.Volume{ //Volumes must be patched 129 | // { 130 | // Name: "cert", 131 | // }, 132 | // }, 133 | }, 134 | }, 135 | }, 136 | } 137 | } 138 | 139 | func (b *baseManifestFactory) serviceAccount() *corev1.ServiceAccount { 140 | return manifests.NewServiceAccountBuilder().Build(b.serviceAccountMeta) 141 | } 142 | 143 | func (b *baseManifestFactory) clusterRole() *rbacv1.ClusterRole { 144 | return &rbacv1.ClusterRole{ 145 | TypeMeta: metav1.TypeMeta{ 146 | APIVersion: rbacv1.SchemeGroupVersion.String(), 147 | Kind: "ClusterRole", 148 | }, 149 | ObjectMeta: metav1.ObjectMeta{ 150 | Name: "pod-identity-webhook", 151 | }, 152 | Rules: []rbacv1.PolicyRule{ 153 | { 154 | APIGroups: []string{""}, 155 | Resources: []string{"secrets"}, 156 | Verbs: []string{"create", "get", "update", "patch"}, 157 | }, 158 | { 159 | APIGroups: []string{""}, 160 | Resources: []string{"serviceaccounts"}, 161 | Verbs: []string{"get", "watch", "list"}, 162 | }, 163 | { 164 | APIGroups: []string{"certificates.k8s.io"}, 165 | Resources: []string{"certificatesigningrequests"}, 166 | Verbs: []string{"create", "get", "list", "watch"}, 167 | }, 168 | }, 169 | } 170 | } 171 | 172 | func (b *baseManifestFactory) clusterRoleBinding() *rbacv1.ClusterRoleBinding { 173 | return &rbacv1.ClusterRoleBinding{ 174 | TypeMeta: metav1.TypeMeta{ 175 | APIVersion: rbacv1.SchemeGroupVersion.String(), 176 | Kind: "ClusterRoleBinding", 177 | }, 178 | ObjectMeta: metav1.ObjectMeta{ 179 | Name: "pod-identity-webhook", 180 | }, 181 | RoleRef: rbacv1.RoleRef{ 182 | APIGroup: rbacv1.SchemeGroupVersion.Group, 183 | Kind: "ClusterRole", 184 | Name: "pod-identity-webhook", 185 | }, 186 | Subjects: []rbacv1.Subject{ 187 | { 188 | Kind: "ServiceAccount", 189 | Name: b.serviceAccountMeta.Name, 190 | Namespace: b.serviceAccountMeta.Namespace, 191 | }, 192 | }, 193 | } 194 | } 195 | 196 | func (b *baseManifestFactory) service() *corev1.Service { 197 | return &corev1.Service{ 198 | TypeMeta: metav1.TypeMeta{ 199 | APIVersion: corev1.SchemeGroupVersion.String(), 200 | Kind: "Service", 201 | }, 202 | ObjectMeta: metav1.ObjectMeta{ 203 | Name: b.serviceMeta.Name, 204 | Namespace: b.serviceMeta.Namespace, 205 | Annotations: map[string]string{ 206 | "prometheus.io/port": "443", 207 | "prometheus.io/scheme": "https", 208 | "prometheus.io/scrape": "true", 209 | }, 210 | }, 211 | Spec: corev1.ServiceSpec{ 212 | Ports: []corev1.ServicePort{ 213 | { 214 | Port: 443, 215 | TargetPort: intstr.FromInt(443), 216 | }, 217 | }, 218 | Selector: b.podLabel, 219 | }, 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/base_manifests_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/goccy/go-yaml" 8 | "github.com/stretchr/testify/assert" 9 | regv1 "k8s.io/api/admissionregistration/v1" 10 | appsv1 "k8s.io/api/apps/v1" 11 | corev1 "k8s.io/api/core/v1" 12 | rbacv1 "k8s.io/api/rbac/v1" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | func TestBaseManifests(t *testing.T) { 17 | b := newBaseManifestFactory() 18 | tests := []struct { 19 | name string 20 | runFunc func() client.Object 21 | expected string 22 | expectedFunc func() client.Object 23 | }{ 24 | { 25 | name: "mutatingwebhookconfiguration", 26 | runFunc: func() client.Object { 27 | return b.mutatingWebhookConfiguration() 28 | }, 29 | expected: "testdata/mutatingwebhook.yaml", 30 | expectedFunc: testMutatingWebhookConfiguration, 31 | }, 32 | { 33 | name: "service", 34 | runFunc: func() client.Object { 35 | return b.service() 36 | }, 37 | expected: "testdata/service.yaml", 38 | expectedFunc: testService, 39 | }, 40 | { 41 | name: "deployment", 42 | runFunc: func() client.Object { 43 | return b.deployment() 44 | }, 45 | expected: "testdata/deployment.yaml", 46 | expectedFunc: testDeployment, 47 | }, 48 | { 49 | name: "serviceaccount", 50 | runFunc: func() client.Object { 51 | return b.serviceAccount() 52 | }, 53 | expected: "testdata/serviceaccount.yaml", 54 | expectedFunc: testServiceAccount, 55 | }, 56 | { 57 | name: "clusterrole", 58 | runFunc: func() client.Object { 59 | return b.clusterRole() 60 | }, 61 | expected: "testdata/clusterrole.yaml", 62 | expectedFunc: testClusterRole, 63 | }, 64 | { 65 | name: "clusterrolebinding", 66 | runFunc: func() client.Object { 67 | return b.clusterRoleBinding() 68 | }, 69 | expected: "testdata/clusterrolebinding.yaml", 70 | expectedFunc: testClusterRoleBinding, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | actual := tt.runFunc() 76 | data, err := os.ReadFile(tt.expected) 77 | assert.NoError(t, err) 78 | expected := tt.expectedFunc() 79 | err = yaml.UnmarshalWithOptions(data, expected, yaml.UseJSONUnmarshaler()) 80 | assert.NoError(t, err) 81 | assert.Equal(t, expected, actual) 82 | }) 83 | } 84 | } 85 | 86 | func testMutatingWebhookConfiguration() client.Object { 87 | return ®v1.MutatingWebhookConfiguration{} 88 | } 89 | 90 | func testService() client.Object { 91 | return &corev1.Service{} 92 | } 93 | 94 | func testDeployment() client.Object { 95 | return &appsv1.Deployment{} 96 | } 97 | 98 | func testServiceAccount() client.Object { 99 | return &corev1.ServiceAccount{} 100 | } 101 | 102 | func testClusterRole() client.Object { 103 | return &rbacv1.ClusterRole{} 104 | } 105 | 106 | func testClusterRoleBinding() client.Object { 107 | return &rbacv1.ClusterRoleBinding{} 108 | } 109 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/certificate.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "math/big" 10 | "time" 11 | 12 | "k8s.io/apimachinery/pkg/types" 13 | ) 14 | 15 | type TlsCredential struct { 16 | privateKey []byte 17 | certificate []byte 18 | } 19 | 20 | func (t TlsCredential) Certificate() []byte { 21 | return t.certificate 22 | } 23 | 24 | func (t TlsCredential) PrivateKey() []byte { 25 | return t.privateKey 26 | } 27 | 28 | func CreateTlsCredential(serviceNamespacedName types.NamespacedName) (TlsCredential, error) { 29 | certificatePeriod := 365 // days 30 | 31 | // Generate RSA private key 32 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 33 | if err != nil { 34 | return TlsCredential{}, err 35 | } 36 | 37 | // Define certificate template 38 | template := x509.Certificate{ 39 | SerialNumber: big.NewInt(1), 40 | Subject: pkix.Name{ 41 | CommonName: serviceNamespacedName.Name + "." + serviceNamespacedName.Namespace + ".svc", 42 | }, 43 | NotBefore: time.Now(), 44 | NotAfter: time.Now().AddDate(0, 0, certificatePeriod), 45 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 46 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 47 | BasicConstraintsValid: true, 48 | IsCA: true, 49 | } 50 | 51 | // Add SANs to the certificate template 52 | template.DNSNames = []string{ 53 | serviceNamespacedName.Name + "." + serviceNamespacedName.Namespace + ".svc", 54 | serviceNamespacedName.Name + "." + serviceNamespacedName.Namespace + ".svc.cluster.local", 55 | } 56 | 57 | // Create the certificate 58 | certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 59 | if err != nil { 60 | return TlsCredential{}, err 61 | } 62 | 63 | // Encode the private key to PEM format 64 | privPemBytes := pem.EncodeToMemory(&pem.Block{ 65 | Type: "RSA PRIVATE KEY", 66 | Bytes: x509.MarshalPKCS1PrivateKey(privateKey), 67 | }) 68 | 69 | // Encode the certificate to PEM format 70 | certPemBytes := pem.EncodeToMemory(&pem.Block{ 71 | Type: "CERTIFICATE", 72 | Bytes: certBytes, 73 | }) 74 | 75 | return TlsCredential{privateKey: privPemBytes, certificate: certPemBytes}, nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/certificate_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "testing" 8 | 9 | "k8s.io/apimachinery/pkg/types" 10 | ) 11 | 12 | func TestCreateTlsCredentials(t *testing.T) { 13 | creds, err := CreateTlsCredential(types.NamespacedName{ 14 | Name: "pod-identity-webhook", 15 | Namespace: "kube-system", 16 | }) 17 | if err != nil { 18 | t.Fatalf("Failed to create TLS credentials: %v", err) 19 | } 20 | 21 | certBlock, _ := pem.Decode(creds.certificate) 22 | var cert *x509.Certificate 23 | if certBlock == nil { 24 | t.Fatal("Failed to decode PEM block containing the certificate") 25 | } else { 26 | cert, err = x509.ParseCertificate(certBlock.Bytes) 27 | if err != nil { 28 | t.Fatalf("Failed to parse certificate: %v", err) 29 | } 30 | } 31 | var key *rsa.PrivateKey 32 | keyBlock, _ := pem.Decode(creds.privateKey) 33 | if keyBlock == nil { 34 | t.Fatal("Failed to decode PEM block containing the private key") 35 | } else { 36 | key, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes) 37 | if err != nil { 38 | t.Fatalf("Failed to parse private key: %v", err) 39 | } 40 | 41 | } 42 | 43 | // Verify public keys are equivalent 44 | if !publicKeysEqual(cert.PublicKey, &key.PublicKey) { 45 | t.Fatal("Public key in certificate does not match public key in private key") 46 | } 47 | } 48 | 49 | // Helper function to compare public keys 50 | func publicKeysEqual(pub1, pub2 interface{}) bool { 51 | rsaPub1, ok1 := pub1.(*rsa.PublicKey) 52 | rsaPub2, ok2 := pub2.(*rsa.PublicKey) 53 | 54 | if !ok1 || !ok2 { 55 | return false 56 | } 57 | return rsaPub1.N.Cmp(rsaPub2.N) == 0 && rsaPub1.E == rsaPub2.E 58 | } 59 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/testdata/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: pod-identity-webhook 5 | rules: 6 | - apiGroups: 7 | - "" 8 | resources: 9 | - secrets 10 | verbs: 11 | - create 12 | - get 13 | - update 14 | - patch 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - serviceaccounts 19 | verbs: 20 | - get 21 | - watch 22 | - list 23 | - apiGroups: 24 | - certificates.k8s.io 25 | resources: 26 | - certificatesigningrequests 27 | verbs: 28 | - create 29 | - get 30 | - list 31 | - watch 32 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/testdata/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: pod-identity-webhook 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: pod-identity-webhook 9 | subjects: 10 | - kind: ServiceAccount 11 | name: pod-identity-webhook 12 | namespace: kube-system 13 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/testdata/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pod-identity-webhook 5 | namespace: kube-system 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: pod-identity-webhook 11 | template: 12 | metadata: 13 | labels: 14 | app: pod-identity-webhook 15 | spec: 16 | serviceAccountName: pod-identity-webhook 17 | containers: 18 | - name: pod-identity-webhook 19 | image: amazon/amazon-eks-pod-identity-webhook:latest 20 | imagePullPolicy: Always 21 | # command: 22 | # - /webhook 23 | # - --in-cluster 24 | # - --namespace=kube-system 25 | # - --service-name=pod-identity-webhook 26 | # - --tls-secret=pod-identity-webhook 27 | # - --annotation-prefix=eks.amazonaws.com 28 | # - --token-audience=sts.amazonaws.com 29 | # - --logtostderr 30 | volumeMounts: 31 | - name: cert 32 | mountPath: /etc/webhook/certs 33 | readOnly: true 34 | # volumes: 35 | # - name: cert 36 | # secret: 37 | # secretName: pod-identity-webhook 38 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/testdata/mutatingwebhook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: pod-identity-webhook 5 | namespace: kube-system 6 | webhooks: 7 | - name: pod-identity-webhook.amazonaws.com 8 | failurePolicy: Ignore 9 | clientConfig: 10 | service: 11 | name: pod-identity-webhook 12 | namespace: kube-system 13 | path: "/mutate" 14 | rules: 15 | - operations: ["CREATE"] 16 | apiGroups: [""] 17 | apiVersions: ["v1"] 18 | resources: ["pods"] 19 | sideEffects: None 20 | admissionReviewVersions: ["v1beta1"] 21 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/testdata/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: pod-identity-webhook 5 | namespace: kube-system 6 | annotations: 7 | prometheus.io/port: "443" 8 | prometheus.io/scheme: "https" 9 | prometheus.io/scrape: "true" 10 | spec: 11 | ports: 12 | - port: 443 13 | targetPort: 443 14 | selector: 15 | app: pod-identity-webhook -------------------------------------------------------------------------------- /internal/selfhosted/webhook/testdata/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: pod-identity-webhook 5 | namespace: kube-system 6 | -------------------------------------------------------------------------------- /internal/selfhosted/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kkb0318/irsa-manager/internal/manifests" 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/types" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | type WebhookSetup struct { 13 | resources []client.Object 14 | } 15 | 16 | func secretNamespacedName() types.NamespacedName { 17 | return types.NamespacedName{ 18 | Name: "pod-identity-webhook", 19 | Namespace: WEBHOOK_NAMESPACE, 20 | } 21 | } 22 | 23 | func (w *WebhookSetup) Resources() []client.Object { 24 | return w.resources 25 | } 26 | 27 | func NewWebHookSetup() (*WebhookSetup, error) { 28 | factory := newBaseManifestFactory() 29 | resources, err := myCertificate(factory) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return &WebhookSetup{resources}, nil 34 | } 35 | 36 | func myCertificate(base *baseManifestFactory) ([]client.Object, error) { 37 | tlsCredential, err := CreateTlsCredential(serviceNamespacedName()) 38 | if err != nil { 39 | return nil, err 40 | } 41 | resources := []client.Object{} 42 | secretNamespacedName := secretNamespacedName() 43 | secret, err := manifests.NewSecretBuilder(). 44 | WithCertificate(tlsCredential). 45 | Build(secretNamespacedName) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | deploy := base.deployment() 51 | deploy.Spec.Template.Spec.Containers[0].Command = []string{ 52 | "/webhook", 53 | "--in-cluster=false", 54 | fmt.Sprintf("--namespace=%s", WEBHOOK_NAMESPACE), 55 | fmt.Sprintf("--service-name=%s", base.serviceMeta.Name), 56 | fmt.Sprintf("--tls-secret=%s", secretNamespacedName.Name), 57 | "--annotation-prefix=eks.amazonaws.com", 58 | "--token-audience=sts.amazonaws.com", 59 | "--logtostderr", 60 | } 61 | deploy.Spec.Template.Spec.Volumes = []corev1.Volume{ 62 | { 63 | Name: "cert", 64 | VolumeSource: corev1.VolumeSource{ 65 | Secret: &corev1.SecretVolumeSource{ 66 | SecretName: secretNamespacedName.Name, 67 | }, 68 | }, 69 | }, 70 | } 71 | mutate := base.mutatingWebhookConfiguration() 72 | mutate.Webhooks[0].ClientConfig.CABundle = tlsCredential.Certificate() 73 | resources = append(resources, 74 | secret, 75 | deploy, 76 | mutate, 77 | base.clusterRole(), 78 | base.clusterRoleBinding(), 79 | base.serviceAccount(), 80 | base.service(), 81 | ) 82 | return resources, nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/utils/diff.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/types" 5 | ) 6 | 7 | // DiffNamespacedNames returns the namespaced names that are in target but not in reference. 8 | func DiffNamespacedNames(target, reference []types.NamespacedName) []types.NamespacedName { 9 | referenceSet := make(map[types.NamespacedName]struct{}) 10 | 11 | for _, item := range reference { 12 | referenceSet[item] = struct{}{} 13 | } 14 | 15 | diff := []types.NamespacedName{} 16 | for _, item := range target { 17 | if _, exists := referenceSet[item]; !exists { 18 | diff = append(diff, item) 19 | } 20 | } 21 | 22 | return diff 23 | } 24 | -------------------------------------------------------------------------------- /internal/utils/diff_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/apimachinery/pkg/types" 8 | ) 9 | 10 | func TestDiffNamespacedNames(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | target []types.NamespacedName 14 | reference []types.NamespacedName 15 | expected []types.NamespacedName 16 | }{ 17 | { 18 | "NoDifference", 19 | []types.NamespacedName{ 20 | {Namespace: "default", Name: "resource1"}, 21 | {Namespace: "default", Name: "resource2"}, 22 | }, 23 | []types.NamespacedName{ 24 | {Namespace: "default", Name: "resource1"}, 25 | {Namespace: "default", Name: "resource2"}, 26 | }, 27 | []types.NamespacedName{}, 28 | }, 29 | { 30 | "SomeDifference", 31 | []types.NamespacedName{ 32 | {Namespace: "default", Name: "resource1"}, 33 | {Namespace: "default", Name: "resource3"}, 34 | }, 35 | []types.NamespacedName{ 36 | {Namespace: "default", Name: "resource1"}, 37 | {Namespace: "default", Name: "resource2"}, 38 | }, 39 | []types.NamespacedName{ 40 | {Namespace: "default", Name: "resource3"}, 41 | }, 42 | }, 43 | { 44 | "AllDifferent", 45 | []types.NamespacedName{ 46 | {Namespace: "default", Name: "resource1"}, 47 | {Namespace: "default", Name: "resource2"}, 48 | }, 49 | []types.NamespacedName{ 50 | {Namespace: "other", Name: "resource3"}, 51 | {Namespace: "other", Name: "resource4"}, 52 | }, 53 | []types.NamespacedName{ 54 | {Namespace: "default", Name: "resource1"}, 55 | {Namespace: "default", Name: "resource2"}, 56 | }, 57 | }, 58 | { 59 | "EmptyTarget", 60 | []types.NamespacedName{}, 61 | []types.NamespacedName{ 62 | {Namespace: "default", Name: "resource1"}, 63 | {Namespace: "default", Name: "resource2"}, 64 | }, 65 | []types.NamespacedName{}, 66 | }, 67 | { 68 | "EmptyReference", 69 | []types.NamespacedName{ 70 | {Namespace: "default", Name: "resource1"}, 71 | {Namespace: "default", Name: "resource2"}, 72 | }, 73 | []types.NamespacedName{}, 74 | []types.NamespacedName{ 75 | {Namespace: "default", Name: "resource1"}, 76 | {Namespace: "default", Name: "resource2"}, 77 | }, 78 | }, 79 | } 80 | 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | result := DiffNamespacedNames(tt.target, tt.reference) 84 | assert.Equal(t, tt.expected, result) 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /validation/job-amd.yaml.template: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: s3-echoer 5 | spec: 6 | template: 7 | spec: 8 | serviceAccountName: irsa1-sa 9 | containers: 10 | - name: main 11 | image: amazonlinux:2018.03 12 | command: 13 | - "sh" 14 | - "-c" 15 | - "curl -sL -o /s3-echoer https://github.com/mhausenblas/s3-echoer/releases/latest/download/s3-echoer-linux && chmod +x /s3-echoer && echo This is an in-cluster test | /s3-echoer TARGET_BUCKET" 16 | env: 17 | - name: AWS_DEFAULT_REGION 18 | value: "ap-northeast-1" 19 | - name: ENABLE_IRP 20 | value: "true" 21 | restartPolicy: Never 22 | -------------------------------------------------------------------------------- /validation/job-arm.yaml.template: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: s3-echoer 5 | spec: 6 | template: 7 | spec: 8 | serviceAccountName: irsa1-sa 9 | containers: 10 | - name: main 11 | image: amazonlinux:2023 12 | command: 13 | - "sh" 14 | - "-c" 15 | - "curl -sL -o /s3-echoer https://github.com/kkb0318/s3-echoer-arm/releases/latest/download/s3-echoer-linux-arm64 && chmod +x /s3-echoer && echo This is an in-cluster test | /s3-echoer TARGET_BUCKET" 16 | env: 17 | - name: AWS_DEFAULT_REGION 18 | value: "ap-northeast-1" 19 | - name: ENABLE_IRP 20 | value: "true" 21 | restartPolicy: Never 22 | -------------------------------------------------------------------------------- /validation/s3-echoer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | TARGET_BUCKET_PREFIX=s3-echoer 5 | AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-ap-northeast-1} 6 | JOB_TEMPLATE=job-arm.yaml.template 7 | 8 | # deploy s3 echoer job into k8s cluster 9 | timestamp=$(date +%s) 10 | TARGET_BUCKET=$ROLE_NAME-$timestamp 11 | 12 | aws s3api create-bucket \ 13 | --bucket $TARGET_BUCKET_PREFIX \ 14 | --create-bucket-configuration LocationConstraint=$AWS_DEFAULT_REGION \ 15 | --region $AWS_DEFAULT_REGION 16 | 17 | sed -e "s/TARGET_BUCKET/${TARGET_BUCKET_PREFIX}/g" ${JOB_TEMPLATE} > s3-echoer-job.yaml 18 | 19 | kubectl create -f s3-echoer-job.yaml 20 | 21 | echo "The S3 bucket is $TARGET_BUCKET_PREFIX" 22 | 23 | --------------------------------------------------------------------------------