├── .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 | [](https://github.com/kkb0318/irsa-manager/releases)
4 | [](https://github.com/kkb0318/irsa-manager/actions/workflows/ci.yaml)
5 | [](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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------