├── .dockerignore ├── .tool-versions ├── e2e ├── pkg │ ├── templates │ │ ├── issuer.yaml.tmpl │ │ ├── template.go │ │ ├── certificate.yaml.tmpl │ │ ├── webhook.yaml.tmpl │ │ └── manifests.go │ ├── util │ │ └── manifests.go │ └── fixtures │ │ └── webhook.go ├── e2e_suite_test.go ├── README.md └── e2e_test.go ├── local_e2e ├── e2e_suite_test.go ├── README.md ├── pkg │ └── fixtures │ │ ├── service.go │ │ ├── ingress.go │ │ ├── manager.go │ │ └── manifests.go └── cluster.yaml ├── main.go ├── .github └── workflows │ ├── reviewdog.yml │ ├── test.yml │ ├── go-mod-fix.yml │ ├── manifests.yml │ ├── docker-publish.yml │ └── e2e.yml ├── config ├── samples │ ├── service.yaml │ ├── endpointgroupbinding.yaml │ ├── deployment.yaml │ ├── alb-internal-ingress.yaml │ ├── alb-public-ingress.yaml │ ├── nlb-public-ip-service.yaml │ ├── nlb-public-service.yaml │ └── nlb-internal-service.yaml ├── webhook │ └── manifests.yaml ├── rbac │ └── role.yaml └── crd │ └── operator.h3poteto.dev_endpointgroupbindings.yaml ├── pkg ├── cloudprovider │ ├── provider.go │ ├── provider_test.go │ └── aws │ │ ├── aws.go │ │ ├── load_balancer_test.go │ │ ├── load_balancer.go │ │ └── route53_test.go ├── errors │ ├── errors.go │ └── errors_test.go ├── apis │ ├── endpointgroupbinding │ │ └── v1alpha1 │ │ │ ├── doc.go │ │ │ ├── registry.go │ │ │ ├── types.go │ │ │ └── zz_generated.deepcopy.go │ └── type.go ├── client │ ├── clientset │ │ └── versioned │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ └── clientset_generated.go │ │ │ ├── scheme │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ ├── typed │ │ │ └── endpointgroupbinding │ │ │ │ └── v1alpha1 │ │ │ │ ├── fake │ │ │ │ ├── doc.go │ │ │ │ ├── fake_endpointgroupbinding_client.go │ │ │ │ └── fake_endpointgroupbinding.go │ │ │ │ ├── doc.go │ │ │ │ ├── generated_expansion.go │ │ │ │ ├── endpointgroupbinding_client.go │ │ │ │ └── endpointgroupbinding.go │ │ │ └── clientset.go │ ├── listers │ │ └── endpointgroupbinding │ │ │ └── v1alpha1 │ │ │ ├── expansion_generated.go │ │ │ └── endpointgroupbinding.go │ └── informers │ │ └── externalversions │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ ├── endpointgroupbinding │ │ ├── v1alpha1 │ │ │ ├── interface.go │ │ │ └── endpointgroupbinding.go │ │ └── interface.go │ │ └── generic.go ├── signals │ └── signals.go ├── fixture │ └── endpointgroupbinding.go ├── manager │ ├── route53.go │ ├── globalaccelerator.go │ ├── endpointgroupbinding_controller.go │ └── manager.go ├── webhoook │ ├── webhook.go │ ├── endpointgroupbinding │ │ └── validator.go │ └── webhook_test.go ├── leaderelection │ └── leaderelection.go ├── reconcile │ └── reconcile.go └── controller │ ├── route53 │ ├── ingress.go │ ├── service.go │ └── controller.go │ ├── globalaccelerator │ ├── service.go │ └── ingress.go │ └── endpointgroupbinding │ ├── controller.go │ └── reconcile.go ├── renovate.json ├── cmd ├── version.go ├── root.go ├── webhook │ └── webhook.go └── controller │ └── controller.go ├── Dockerfile ├── boilerplate.go.txt ├── hack ├── update-codegen.sh └── kind-with-registry.sh ├── Makefile └── go.mod /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.24.5 2 | -------------------------------------------------------------------------------- /e2e/pkg/templates/issuer.yaml.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Issuer 3 | metadata: 4 | name: {{ .IssuerName }} 5 | namespace: {{ .Namespace }} 6 | spec: 7 | selfSigned: {} -------------------------------------------------------------------------------- /e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestE2e(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "E2e Suite") 13 | } 14 | -------------------------------------------------------------------------------- /local_e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestE2e(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "E2e Suite") 13 | } 14 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/h3poteto/aws-global-accelerator-controller/cmd" 8 | ) 9 | 10 | func main() { 11 | if err := cmd.RootCmd.Execute(); err != nil { 12 | fmt.Println(err) 13 | os.Exit(-1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /e2e/pkg/templates/template.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import _ "embed" 4 | 5 | //go:embed issuer.yaml.tmpl 6 | var issuerTmpl string 7 | 8 | //go:embed certificate.yaml.tmpl 9 | var certificateTmpl string 10 | 11 | //go:embed webhook.yaml.tmpl 12 | var webhookTmpl string 13 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | name: reviewdog 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | reviewdog: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v5 11 | - name: golangci-lint 12 | uses: reviewdog/action-golangci-lint@v2 13 | with: 14 | golangci_lint_flags: "--timeout 5m" 15 | -------------------------------------------------------------------------------- /config/samples/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: h3poteto-test 5 | namespace: default 6 | spec: 7 | ports: 8 | - name: http 9 | port: 80 10 | protocol: TCP 11 | targetPort: 80 12 | - name: https 13 | port: 443 14 | protocol: TCP 15 | targetPort: 443 16 | type: NodePort 17 | selector: 18 | app: h3poteto 19 | -------------------------------------------------------------------------------- /config/samples/endpointgroupbinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operator.h3poteto.dev/v1alpha1 2 | kind: EndpointGroupBinding 3 | metadata: 4 | name: h3poteto-test 5 | namespace: default 6 | spec: 7 | endpointGroupArn: arn:aws:globalaccelerator::564677439943:accelerator/d113ae41-29df-496d-b8d2-316b322764bf/listener/f1fe2960/endpoint-group/eea4ad34cd00 8 | weight: 100 9 | serviceRef: 10 | name: h3poteto-test 11 | -------------------------------------------------------------------------------- /e2e/pkg/templates/certificate.yaml.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: {{ .CertificateName }} 5 | namespace: {{ .Namespace }} 6 | spec: 7 | dnsNames: 8 | - {{ .ServiceName }}.{{ .Namespace }}.svc 9 | - {{ .ServiceName }}.{{ .Namespace }}.svc.cluster.local 10 | issuerRef: 11 | kind: Issuer 12 | name: {{ .IssuerName }} 13 | secretName: {{ .CertSecretName }} -------------------------------------------------------------------------------- /pkg/cloudprovider/provider.go: -------------------------------------------------------------------------------- 1 | package cloudprovider 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func DetectCloudProvider(hostname string) (string, error) { 9 | parts := strings.Split(hostname, ".") 10 | domain := parts[len(parts)-2] + "." + parts[len(parts)-1] 11 | switch domain { 12 | case "amazonaws.com": 13 | return "aws", nil 14 | default: 15 | return "", fmt.Errorf("Unknown cloud provider: %s", domain) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | 10 | jobs: 11 | unit-test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@master 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version-file: "go.mod" 19 | - name: Testing 20 | run: | 21 | go mod download 22 | go test ./pkg/... 23 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "postUpdateOptions": [ 6 | "gomodTidy" 7 | ], 8 | "packageRules": [ 9 | { 10 | "groupName": "k8s.io (major or minor)", 11 | "matchPackageNames": [ 12 | "k8s.io/api", 13 | "k8s.io/apimachinery", 14 | "k8s.io/client-go", 15 | "k8s.io/kubectl" 16 | ], 17 | "matchUpdateTypes": ["patch"], 18 | "enabled": false 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | version string 11 | revision string 12 | build string 13 | ) 14 | 15 | func versionCmd() *cobra.Command { 16 | cmd := &cobra.Command{ 17 | Use: "version", 18 | Short: "Print the version number", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fmt.Printf("Version : %s\n", version) 21 | fmt.Printf("Revision: %s\n", revision) 22 | fmt.Printf("Build : %s\n", build) 23 | }, 24 | } 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/go-mod-fix.yml: -------------------------------------------------------------------------------- 1 | 2 | name: go-mod-fix 3 | on: 4 | push: 5 | branches: 6 | - renovate/* 7 | 8 | jobs: 9 | go-mod-fix: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v5 14 | with: 15 | fetch-depth: 2 16 | - name: fix 17 | uses: at-wat/go-sum-fix-action@v0 18 | with: 19 | git_user: h3poteto 20 | git_email: h3.poteto@gmail.com 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | commit_style: squash 23 | push: force 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.24 as builder 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /go/src/github.com/h3poteto/aws-global-accelerator-controller 7 | 8 | COPY go.mod . 9 | COPY go.sum . 10 | RUN go mod download 11 | 12 | COPY . . 13 | RUN set -ex && \ 14 | TARGETOS="$TARGETOS" TARGETARCH="$TARGETARCH" \ 15 | make build 16 | 17 | FROM gcr.io/distroless/static:nonroot 18 | WORKDIR / 19 | COPY --from=builder /go/src/github.com/h3poteto/aws-global-accelerator-controller/aws-global-accelerator-controller . 20 | USER nonroot:nonroot 21 | 22 | CMD ["/aws-global-accelerator-controller"] 23 | -------------------------------------------------------------------------------- /boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | -------------------------------------------------------------------------------- /.github/workflows/manifests.yml: -------------------------------------------------------------------------------- 1 | name: Manifests 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | manifests: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@master 12 | - uses: actions/setup-go@v5 13 | with: 14 | go-version-file: "go.mod" 15 | - name: Generate manifests 16 | id: manifests 17 | run: | 18 | make manifests 19 | diff=$(git status --short | wc -l) && echo "::set-output name=DIFF::$diff" 20 | - name: 21 | if: steps.manifests.outputs.DIFF != 0 22 | run: | 23 | echo "Manifests diff exists" 24 | git status 25 | exit 1 26 | 27 | -------------------------------------------------------------------------------- /config/samples/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: h3poteto 5 | namespace: default 6 | labels: 7 | app: h3poteto 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: h3poteto 13 | template: 14 | metadata: 15 | labels: 16 | app: h3poteto 17 | spec: 18 | containers: 19 | - name: nginx 20 | image: nginx:latest 21 | imagePullPolicy: Always 22 | ports: 23 | - name: http-port 24 | containerPort: 80 25 | protocol: TCP 26 | - name: https-port 27 | containerPort: 443 28 | protocol: TCP 29 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: ValidatingWebhookConfiguration 4 | metadata: 5 | name: validating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /validate-endpointgroupbinding 14 | failurePolicy: Fail 15 | name: validate-endpointgroupbinding.h3poteto.dev 16 | rules: 17 | - apiGroups: 18 | - operator.h3poteto.dev 19 | apiVersions: 20 | - v1alpha1 21 | operations: 22 | - CREATE 23 | - UPDATE 24 | resources: 25 | - endpointgroupbindings 26 | sideEffects: None 27 | -------------------------------------------------------------------------------- /pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | type NoRetryError struct { 9 | msg string 10 | err error 11 | } 12 | 13 | func NewNoRetryError(msg string) *NoRetryError { 14 | return &NoRetryError{ 15 | msg: msg, 16 | } 17 | } 18 | 19 | func NewNoRetryErrorf(format string, v ...interface{}) *NoRetryError { 20 | return &NoRetryError{ 21 | msg: fmt.Sprintf(format, v...), 22 | } 23 | } 24 | 25 | func (e *NoRetryError) Error() string { 26 | return e.msg 27 | } 28 | 29 | func (e *NoRetryError) Unwrap() error { 30 | return e.err 31 | } 32 | 33 | func IsNoRetry(err error) bool { 34 | noRetry := &NoRetryError{} 35 | if errors.As(err, &noRetry) { 36 | return true 37 | } 38 | return false 39 | } 40 | -------------------------------------------------------------------------------- /config/samples/alb-internal-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: h3poteto-test 5 | namespace: default 6 | annotations: 7 | aws-global-accelerator-controller.h3poteto.dev/global-accelerator-managed: "yes" 8 | aws-global-accelerator-controller.h3poteto.dev/route53-hostname: "foo.h3poteto-test.dev,bar.h3poteto-test.dev" 9 | alb.ingress.kubernetes.io/scheme: internal 10 | alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]' 11 | spec: 12 | ingressClassName: alb 13 | rules: 14 | - http: 15 | paths: 16 | - pathType: Prefix 17 | path: "/" 18 | backend: 19 | service: 20 | name: h3poteto-test 21 | port: 22 | number: 80 23 | 24 | -------------------------------------------------------------------------------- /config/samples/alb-public-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: h3poteto-test 5 | namespace: default 6 | annotations: 7 | aws-global-accelerator-controller.h3poteto.dev/global-accelerator-managed: "yes" 8 | aws-global-accelerator-controller.h3poteto.dev/route53-hostname: "foo.h3poteto-test.dev,bar.h3poteto-test.dev" 9 | alb.ingress.kubernetes.io/scheme: internet-facing 10 | alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]' 11 | spec: 12 | ingressClassName: alb 13 | rules: 14 | - http: 15 | paths: 16 | - pathType: Prefix 17 | path: "/" 18 | backend: 19 | service: 20 | name: h3poteto-test 21 | port: 22 | number: 80 23 | 24 | -------------------------------------------------------------------------------- /pkg/apis/endpointgroupbinding/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | 3 | // Package v1alpha1 is the v1alpha1 version of the API. 4 | // +kubebuilder:object:generate=true 5 | // +groupName=operator.h3poteto.dev 6 | package v1alpha1 7 | 8 | import ( 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | ) 12 | 13 | var SchemeGroupVersion = schema.GroupVersion{Group: "operator.h3poteto.dev", Version: "v1alpha1"} 14 | 15 | var ( 16 | // TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api. 17 | // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. 18 | SchemeBuilder runtime.SchemeBuilder 19 | localSchemeBuilder = &SchemeBuilder 20 | AddToScheme = localSchemeBuilder.AddToScheme 21 | ) 22 | -------------------------------------------------------------------------------- /local_e2e/README.md: -------------------------------------------------------------------------------- 1 | # How to run E2E tests in local 2 | 3 | ## Pre-requirements 4 | - [kops](https://github.com/kubernetes/kops) 5 | - [ginkgo v2](https://github.com/onsi/ginkgo) 6 | 7 | ## Prepare a Kubernetes Cluster 8 | Please rewrite `e2e/cluster.yaml` for your environment, and create a cluster. 9 | 10 | ``` 11 | $ kops create -f e2e/cluster.yaml 12 | $ kops create secret --name $CLUSTER_NAME sshpublickey admin -i ~/.ssh/id_rsa.pub 13 | $ kops update cluster --name $CLUSTER_NAME --yes --admin 14 | $ kops validate cluster --name $CLUSTER_NAME --wait 10m 15 | ``` 16 | 17 | Wait until creating the cluster. 18 | 19 | ## Run E2E test 20 | 21 | ``` 22 | $ E2E_HOSTNAME=foo.h3poteto-test.dev E2E_MANAGER_IMAGE=ghcr.io/h3poteto/aws-global-accelerator-controller:latest ginkgo -r ./e2e 23 | ``` 24 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated fake clientset. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package contains the scheme of the automatically generated clientset. 20 | package scheme 21 | -------------------------------------------------------------------------------- /e2e/pkg/templates/webhook.yaml.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: ValidatingWebhookConfiguration 4 | metadata: 5 | name: validating-webhook-configuration 6 | annotations: 7 | cert-manager.io/inject-ca-from: {{ .CertificateNamespace }}/{{ .CertificateName }} 8 | webhooks: 9 | - admissionReviewVersions: 10 | - v1 11 | clientConfig: 12 | service: 13 | name: {{ .ServiceName }} 14 | namespace: {{ .ServiceNamespace }} 15 | path: {{ .ServiceEndpoint }} 16 | failurePolicy: Fail 17 | name: validate-endpointgroupbinding.h3poteto.dev 18 | rules: 19 | - apiGroups: 20 | - operator.h3poteto.dev 21 | apiVersions: 22 | - v1alpha1 23 | operations: 24 | - CREATE 25 | - UPDATE 26 | resources: 27 | - endpointgroupbindings 28 | sideEffects: None 29 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // Package fake has the automatically generated clients. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated typed clients. 20 | package v1alpha1 21 | -------------------------------------------------------------------------------- /pkg/signals/signals.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | ) 8 | 9 | var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM} 10 | 11 | var onlyOneSignalHandler = make(chan struct{}) 12 | 13 | // SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned 14 | // which is closed on one of these signals. If a second signal is caught, the program 15 | // is terminated with exit code 1. 16 | func SetupSignalHandler() (stopCh <-chan struct{}) { 17 | close(onlyOneSignalHandler) // panics when called twice 18 | 19 | stop := make(chan struct{}) 20 | c := make(chan os.Signal, 2) 21 | signal.Notify(c, shutdownSignals...) 22 | go func() { 23 | <-c 24 | close(stop) 25 | <-c 26 | os.Exit(1) // second signal. Exit directly. 27 | }() 28 | 29 | return stop 30 | } 31 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/h3poteto/aws-global-accelerator-controller/cmd/controller" 7 | "github.com/h3poteto/aws-global-accelerator-controller/cmd/webhook" 8 | "github.com/spf13/cobra" 9 | "k8s.io/klog/v2" 10 | ) 11 | 12 | // RootCmd is cobra command. 13 | var RootCmd = &cobra.Command{ 14 | Use: "aws-global-accelerator-controller", 15 | Short: "aws-global-accelerator-controller is a to manage AWS Global Accelerator from Kubernetes", 16 | SilenceErrors: true, 17 | SilenceUsage: true, 18 | } 19 | 20 | func init() { 21 | klog.InitFlags(flag.CommandLine) 22 | 23 | cobra.OnInitialize() 24 | RootCmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) 25 | RootCmd.AddCommand( 26 | controller.ControllerCmd(), 27 | webhook.WebhookCmd(), 28 | versionCmd(), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | type EndpointGroupBindingExpansion interface{} 22 | -------------------------------------------------------------------------------- /pkg/fixture/endpointgroupbinding.go: -------------------------------------------------------------------------------- 1 | package fixture 2 | 3 | import ( 4 | endpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis/endpointgroupbinding/v1alpha1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | func EndpointGroupBinding(clientIPPreservation bool, service string, weight *int32, arn string) endpointgroupbindingv1alpha1.EndpointGroupBinding { 9 | return endpointgroupbindingv1alpha1.EndpointGroupBinding{ 10 | ObjectMeta: metav1.ObjectMeta{ 11 | Name: "test-endpointgroupbinding", 12 | }, 13 | Spec: endpointgroupbindingv1alpha1.EndpointGroupBindingSpec{ 14 | EndpointGroupArn: arn, 15 | ClientIPPreservation: clientIPPreservation, 16 | Weight: weight, 17 | ServiceRef: &endpointgroupbindingv1alpha1.ServiceReference{ 18 | Name: service, 19 | }, 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 1 | # How to run E2E tests in local 2 | ## Setup kind 3 | 4 | Install kind, please refer: https://kind.sigs.k8s.io/ 5 | 6 | And bootstrap a cluster. 7 | 8 | ``` 9 | $ K8S_VERSION=1.29.4 ./hack/kind-with-registry.sh 10 | ``` 11 | 12 | Install cert-manager 13 | 14 | ``` 15 | $ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.0/cert-manager.crds.yaml 16 | $ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.0/cert-manager.yaml 17 | ``` 18 | 19 | ## Build docker and push 20 | ``` 21 | $ export IMAGE_ID=localhost:5000/aws-global-accelerator-controller 22 | $ docker build . --tag ${IMAGE_ID}:e2e 23 | $ docker push ${IMAGE_ID}:e2e 24 | ``` 25 | 26 | ## Execute e2e test 27 | ``` 28 | $ go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo 29 | $ export WEBHOOK_IMAGE=${IMAGE_ID}:e2e 30 | $ ginkgo -r ./e2e 31 | ``` 32 | -------------------------------------------------------------------------------- /pkg/manager/route53.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | clientset "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned" 5 | ownInformers "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/informers/externalversions" 6 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/controller/route53" 7 | 8 | "k8s.io/client-go/informers" 9 | "k8s.io/client-go/kubernetes" 10 | ) 11 | 12 | func startRoute53Controller(kubeClient kubernetes.Interface, _ clientset.Interface, informerFactory informers.SharedInformerFactory, _ ownInformers.SharedInformerFactory, config *ControllerConfig, stopCh <-chan struct{}, done func()) (bool, error) { 13 | c := route53.NewRoute53Controller(kubeClient, informerFactory, config.Route53) 14 | go func() { 15 | defer done() 16 | c.Run(config.Route53.Workers, stopCh) 17 | }() 18 | return true, nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/apis/type.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | const ( 4 | AWSGlobalAcceleratorManagedAnnotation = "aws-global-accelerator-controller.h3poteto.dev/global-accelerator-managed" 5 | Route53HostnameAnnotation = "aws-global-accelerator-controller.h3poteto.dev/route53-hostname" 6 | ClientIPPreservationAnnotation = "aws-global-accelerator-controller.h3poteto.dev/client-ip-preservation" 7 | AWSGlobalAcceleratorNameAnnotation = "aws-global-accelerator-controller.h3poteto.dev/global-accelerator-name" 8 | AWSGlobalAcceleratorTagsAnnotation = "aws-global-accelerator-controller.h3poteto.dev/global-accelerator-tags" 9 | AWSGlobalAcceleratorIpAddressTypeAnnotation = "aws-global-accelerator-controller.h3poteto.dev/ip-address-type" 10 | 11 | AWSLoadBalancerTypeAnnotation = "service.beta.kubernetes.io/aws-load-balancer-type" 12 | IngressClassAnnotation = "kubernetes.io/ingress.class" 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/errors/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestIsNoRetry(t *testing.T) { 12 | cases := []struct { 13 | title string 14 | err error 15 | expected bool 16 | }{ 17 | { 18 | title: "Error is NoRetryError", 19 | err: &NoRetryError{ 20 | msg: "hoge", 21 | }, 22 | expected: true, 23 | }, 24 | { 25 | title: "Error is NoRetryError with wrapped", 26 | err: fmt.Errorf("my error %w", &NoRetryError{ 27 | msg: "hoge", 28 | }), 29 | expected: true, 30 | }, 31 | { 32 | title: "Error is not NoRetryError", 33 | err: errors.New("my error"), 34 | expected: false, 35 | }, 36 | } 37 | 38 | for _, c := range cases { 39 | t.Run(c.title, func(tt *testing.T) { 40 | actual := IsNoRetry(c.err) 41 | assert.Equal(tt, c.expected, actual) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/cloudprovider/provider_test.go: -------------------------------------------------------------------------------- 1 | package cloudprovider 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | ) 7 | 8 | func TestDetectCloudProvider(t *testing.T) { 9 | cases := []struct { 10 | title string 11 | hostname string 12 | expectedProvider string 13 | }{ 14 | { 15 | title: "NetworkLoadBalancer in AWS", 16 | hostname: "aa5849cde256f49faa7487bb433155b7-3f43353a6cb6f633.elb.ap-northeast-1.amazonaws.com", 17 | expectedProvider: "aws", 18 | }, 19 | } 20 | for _, c := range cases { 21 | log.Printf("Running CASE: %s", c.title) 22 | provider, err := DetectCloudProvider(c.hostname) 23 | if err != nil { 24 | t.Errorf("CASE %s: error has occur: %v", c.title, err) 25 | continue 26 | } 27 | if provider != c.expectedProvider { 28 | t.Errorf("CASE %s: provider is not matched, expected %s, but actual %s", c.title, c.expectedProvider, provider) 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /pkg/manager/globalaccelerator.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | clientset "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned" 5 | ownInformers "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/informers/externalversions" 6 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/controller/globalaccelerator" 7 | 8 | "k8s.io/client-go/informers" 9 | "k8s.io/client-go/kubernetes" 10 | ) 11 | 12 | func startGlobalAcceleratorController(kubeclient kubernetes.Interface, _ clientset.Interface, informerFactory informers.SharedInformerFactory, _ ownInformers.SharedInformerFactory, config *ControllerConfig, stopCh <-chan struct{}, done func()) (bool, error) { 13 | c := globalaccelerator.NewGlobalAcceleratorController(kubeclient, informerFactory, config.GlobalAccelerator) 14 | go func() { 15 | defer done() 16 | c.Run(config.GlobalAccelerator.Workers, stopCh) 17 | }() 18 | return true, nil 19 | } 20 | -------------------------------------------------------------------------------- /hack/update-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 8 | CODEGEN_PKG=${CODE_GENERATOR:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} 9 | 10 | # Ensure we use the correct Go version if asdf is available 11 | if command -v asdf >/dev/null 2>&1; then 12 | export PATH="$(asdf exec go env GOBIN):$(asdf exec go env GOROOT)/bin:$PATH" 13 | fi 14 | 15 | source "${CODEGEN_PKG}/kube_codegen.sh" 16 | 17 | kube::codegen::gen_helpers \ 18 | --boilerplate "${SCRIPT_ROOT}/boilerplate.go.txt" \ 19 | "${SCRIPT_ROOT}/pkg/apis" 20 | 21 | kube::codegen::gen_client \ 22 | --with-watch \ 23 | --output-dir "${SCRIPT_ROOT}/pkg/client" \ 24 | --output-pkg "github.com/h3poteto/aws-global-accelerator-controller/pkg/client" \ 25 | --boilerplate "${SCRIPT_ROOT}/boilerplate.go.txt" \ 26 | "${SCRIPT_ROOT}/pkg/apis" 27 | -------------------------------------------------------------------------------- /config/samples/nlb-public-ip-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp 6 | service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true" 7 | service.beta.kubernetes.io/aws-load-balancer-type: external 8 | service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing 9 | service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip 10 | service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: "HTTP" 11 | service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "traffic-port" 12 | service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: "/" 13 | name: h3poteto-test 14 | namespace: default 15 | spec: 16 | ports: 17 | - name: http 18 | port: 80 19 | protocol: TCP 20 | targetPort: 80 21 | selector: 22 | app: h3poteto 23 | sessionAffinity: None 24 | type: LoadBalancer 25 | -------------------------------------------------------------------------------- /pkg/manager/endpointgroupbinding_controller.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | clientset "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned" 5 | ownInformers "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/informers/externalversions" 6 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/controller/endpointgroupbinding" 7 | "k8s.io/client-go/informers" 8 | "k8s.io/client-go/kubernetes" 9 | ) 10 | 11 | func startEndpointGroupBindingController(kubeclient kubernetes.Interface, ownClient clientset.Interface, informerFactory informers.SharedInformerFactory, ownInformerFactory ownInformers.SharedInformerFactory, config *ControllerConfig, stopCh <-chan struct{}, done func()) (bool, error) { 12 | c := endpointgroupbinding.NewEndpointGroupBindingController(kubeclient, ownClient, informerFactory, ownInformerFactory, config.EndpointGroupBinding) 13 | go func() { 14 | defer done() 15 | c.Run(config.EndpointGroupBinding.Workers, stopCh) 16 | }() 17 | return true, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/cloudprovider/aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/config" 7 | elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" 8 | "github.com/aws/aws-sdk-go-v2/service/globalaccelerator" 9 | "github.com/aws/aws-sdk-go-v2/service/route53" 10 | ) 11 | 12 | type AWS struct { 13 | lb *elbv2.Client 14 | ga *globalaccelerator.Client 15 | route53 *route53.Client 16 | } 17 | 18 | func NewAWS(region string) (*AWS, error) { 19 | cfg, err := config.LoadDefaultConfig(context.Background()) 20 | if err != nil { 21 | return nil, err 22 | } 23 | lb := elbv2.NewFromConfig(cfg, func(o *elbv2.Options) { 24 | o.Region = region 25 | }) 26 | // Global Accelerator requires us-west-2 region, because it is global object. 27 | ga := globalaccelerator.NewFromConfig(cfg, func(o *globalaccelerator.Options) { 28 | o.Region = "us-west-2" 29 | }) 30 | route53 := route53.NewFromConfig(cfg, func(o *route53.Options) { 31 | o.Region = "us-west-2" 32 | }) 33 | return &AWS{ 34 | lb: lb, 35 | ga: ga, 36 | route53: route53, 37 | }, nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/client/listers/endpointgroupbinding/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | // EndpointGroupBindingListerExpansion allows custom methods to be added to 22 | // EndpointGroupBindingLister. 23 | type EndpointGroupBindingListerExpansion interface{} 24 | 25 | // EndpointGroupBindingNamespaceListerExpansion allows custom methods to be added to 26 | // EndpointGroupBindingNamespaceLister. 27 | type EndpointGroupBindingNamespaceListerExpansion interface{} 28 | -------------------------------------------------------------------------------- /pkg/apis/endpointgroupbinding/v1alpha1/registry.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | func init() { 10 | // We only register manually written functions here. The registration of the 11 | // generated functions takes place in the generated files. The separation 12 | // makes the code compile even when the generated files are missing. 13 | localSchemeBuilder.Register(addKnownTypes) 14 | } 15 | 16 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 17 | func Resource(resource string) schema.GroupResource { 18 | return SchemeGroupVersion.WithResource(resource).GroupResource() 19 | } 20 | 21 | // Adds the list of known types to the given scheme. 22 | func addKnownTypes(scheme *runtime.Scheme) error { 23 | scheme.AddKnownTypes(SchemeGroupVersion, 24 | &EndpointGroupBinding{}, 25 | &EndpointGroupBindingList{}, 26 | ) 27 | 28 | scheme.AddKnownTypes(SchemeGroupVersion, 29 | &metav1.Status{}, 30 | ) 31 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: global-accelerator-manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - patch 26 | - update 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | - patch 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - services 38 | verbs: 39 | - get 40 | - list 41 | - watch 42 | - apiGroups: 43 | - coordination.k8s.io 44 | resources: 45 | - leases 46 | verbs: 47 | - create 48 | - delete 49 | - get 50 | - list 51 | - patch 52 | - update 53 | - watch 54 | - apiGroups: 55 | - networking.k8s.io 56 | resources: 57 | - ingresses 58 | verbs: 59 | - get 60 | - list 61 | - watch 62 | - apiGroups: 63 | - operator.h3poteto.dev 64 | resources: 65 | - endpointgroupbindings 66 | verbs: 67 | - create 68 | - delete 69 | - get 70 | - list 71 | - patch 72 | - update 73 | - watch 74 | - apiGroups: 75 | - operator.h3poteto.dev 76 | resources: 77 | - endpointgroupbindings/status 78 | verbs: 79 | - get 80 | - patch 81 | - update 82 | -------------------------------------------------------------------------------- /config/samples/nlb-public-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | aws-global-accelerator-controller.h3poteto.dev/global-accelerator-managed: "yes" 6 | aws-global-accelerator-controller.h3poteto.dev/route53-hostname: "*.hoge.h3poteto-test.dev" 7 | aws-global-accelerator-controller.h3poteto.dev/global-accelerator-name: "h3poteto-test" 8 | aws-global-accelerator-controller.h3poteto.dev/global-accelerator-tags: "Environment=foo,Service=bar" 9 | service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp 10 | service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true" 11 | service.beta.kubernetes.io/aws-load-balancer-type: external 12 | service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing 13 | service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance 14 | service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: "HTTP" 15 | service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "traffic-port" 16 | service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: "/" 17 | name: h3poteto-test 18 | namespace: default 19 | spec: 20 | externalTrafficPolicy: Local 21 | ports: 22 | - name: http 23 | port: 80 24 | protocol: TCP 25 | targetPort: 80 26 | selector: 27 | app: h3poteto 28 | sessionAffinity: None 29 | type: LoadBalancer 30 | -------------------------------------------------------------------------------- /config/samples/nlb-internal-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | aws-global-accelerator-controller.h3poteto.dev/global-accelerator-managed: "yes" 6 | aws-global-accelerator-controller.h3poteto.dev/route53-hostname: "*.hoge.h3poteto-test.dev" 7 | aws-global-accelerator-controller.h3poteto.dev/client-ip-preservation: "true" 8 | service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp 9 | service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true" 10 | service.beta.kubernetes.io/aws-load-balancer-type: external 11 | service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing 12 | service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance 13 | service.beta.kubernetes.io/aws-load-balancer-security-groups: sg-0926c97fdfc296a72 14 | service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: "HTTP" 15 | service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "traffic-port" 16 | service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: "/" 17 | name: h3poteto-test 18 | namespace: default 19 | spec: 20 | externalTrafficPolicy: Local 21 | ports: 22 | - name: http 23 | port: 80 24 | protocol: TCP 25 | targetPort: 80 26 | - name: https 27 | port: 443 28 | protocol: TCP 29 | targetPort: 443 30 | selector: 31 | app: h3poteto 32 | sessionAffinity: None 33 | type: LoadBalancer 34 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1/fake/fake_endpointgroupbinding_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | v1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1" 23 | rest "k8s.io/client-go/rest" 24 | testing "k8s.io/client-go/testing" 25 | ) 26 | 27 | type FakeOperatorV1alpha1 struct { 28 | *testing.Fake 29 | } 30 | 31 | func (c *FakeOperatorV1alpha1) EndpointGroupBindings(namespace string) v1alpha1.EndpointGroupBindingInterface { 32 | return newFakeEndpointGroupBindings(c, namespace) 33 | } 34 | 35 | // RESTClient returns a RESTClient that is used to communicate 36 | // with API server by this client implementation. 37 | func (c *FakeOperatorV1alpha1) RESTClient() rest.Interface { 38 | var ret *rest.RESTClient 39 | return ret 40 | } 41 | -------------------------------------------------------------------------------- /cmd/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | server "github.com/h3poteto/aws-global-accelerator-controller/pkg/webhoook" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | type options struct { 9 | tlsCertFile string 10 | tlsKeyFile string 11 | enableSSL bool 12 | port int32 13 | } 14 | 15 | // +kubebuilder:webhook:path=/validate-endpointgroupbinding,mutating=false,failurePolicy=fail,sideEffects=None,groups=operator.h3poteto.dev,resources=endpointgroupbindings,verbs=create;update,versions=v1alpha1,name=validate-endpointgroupbinding.h3poteto.dev,admissionReviewVersions=v1 16 | 17 | func WebhookCmd() *cobra.Command { 18 | o := &options{} 19 | cmd := &cobra.Command{ 20 | Use: "webhook", 21 | Short: "Start webhook server", 22 | Run: o.run, 23 | } 24 | flags := cmd.Flags() 25 | flags.StringVar(&o.tlsCertFile, "tls-cert-file", "", "File containing the x509 Certificate for HTTPS.") 26 | flags.StringVar(&o.tlsKeyFile, "tls-private-key-file", "", "File containing the x509 private key to --tls-cert-file.") 27 | flags.Int32Var(&o.port, "port", 8443, "Webhook server port.") 28 | flags.BoolVar(&o.enableSSL, "ssl", true, "Webhook server use SSL.") 29 | 30 | return cmd 31 | } 32 | 33 | func (o *options) run(cmd *cobra.Command, args []string) { 34 | if o.enableSSL && (o.tlsCertFile == "" || o.tlsKeyFile == "") { 35 | cmd.PrintErr("You must set --tls-cert-file and --tls-private-key-file when you use SSL\n") 36 | cmd.Help() 37 | return 38 | } 39 | 40 | server.Server(o.port, o.tlsCertFile, o.tlsKeyFile) 41 | } 42 | -------------------------------------------------------------------------------- /hack/kind-with-registry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## Please refer: https://kind.sigs.k8s.io/docs/user/local-registry/ 4 | set -o errexit 5 | 6 | # create registry container unless it already exists 7 | reg_name='kind-registry' 8 | reg_port='5000' 9 | running="$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" 10 | if [ "${running}" != 'true' ]; then 11 | docker run \ 12 | -d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \ 13 | registry:2 14 | fi 15 | 16 | # create a cluster with the local registry enabled in containerd 17 | cat </ 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | 18 | jobs: 19 | build: 20 | 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | packages: write 25 | # This is used to complete the identity challenge 26 | # with sigstore/fulcio when running outside of PRs. 27 | id-token: write 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v5 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Setup Docker buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | # Login against a Docker registry except on PR 40 | # https://github.com/docker/login-action 41 | - name: Log into registry ${{ env.REGISTRY }} 42 | if: github.event_name != 'pull_request' 43 | uses: docker/login-action@v3 44 | with: 45 | registry: ${{ env.REGISTRY }} 46 | username: ${{ github.actor }} 47 | password: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | # Extract metadata (tags, labels) for Docker 50 | # https://github.com/docker/metadata-action 51 | - name: Extract Docker metadata 52 | id: meta 53 | uses: docker/metadata-action@v5 54 | with: 55 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 56 | tags: | 57 | type=ref,event=branch 58 | type=ref,event=pr 59 | type=semver,pattern={{version}} 60 | type=semver,pattern={{major}}.{{minor}} 61 | 62 | # Build and push Docker image with Buildx (don't push on PR) 63 | # https://github.com/docker/build-push-action 64 | - name: Build and push Docker image 65 | id: build-and-push 66 | uses: docker/build-push-action@v6 67 | with: 68 | context: . 69 | platforms: linux/amd64,linux/arm64 70 | push: ${{ github.event_name != 'pull_request' }} 71 | tags: ${{ steps.meta.outputs.tags }} 72 | labels: ${{ steps.meta.outputs.labels }} 73 | -------------------------------------------------------------------------------- /pkg/apis/endpointgroupbinding/v1alpha1/types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 9 | // +kubebuilder:object:root=true 10 | 11 | // EndpointGroupBinding 12 | // +kubebuilder:subresource:status 13 | // +kubebuilder:printcolumn:name="EndpointGroupArn",type=string,JSONPath=`.spec.endpointGroupArn` 14 | // +kubebuilder:printcolumn:name="EndpointIds",type=string,JSONPath=`.status.endpointIds` 15 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 16 | type EndpointGroupBinding struct { 17 | metav1.TypeMeta `json:",inline"` 18 | metav1.ObjectMeta `json:"metadata,omitempty"` 19 | 20 | Spec EndpointGroupBindingSpec `json:"spec,omitempty"` 21 | Status EndpointGroupBindingStatus `json:"status,omitempty"` 22 | } 23 | 24 | type EndpointGroupBindingSpec struct { 25 | // +kubebuilder:validation:Required 26 | // +kubebuilder:validation:Type:=string 27 | EndpointGroupArn string `json:"endpointGroupArn"` 28 | // +optional 29 | // +kubebuilder:validation:Type:=boolean 30 | // +kubebuilder:default=false 31 | ClientIPPreservation bool `json:"clientIPPreservation"` 32 | // +optional 33 | // +nullable 34 | // +kubebuilder:validation:Type:=integer 35 | Weight *int32 `json:"weight"` 36 | 37 | // +optional 38 | ServiceRef *ServiceReference `json:"serviceRef"` 39 | // +optional 40 | IngressRef *IngressReference `json:"ingressRef"` 41 | } 42 | 43 | type ServiceReference struct { 44 | Name string `json:"name"` 45 | } 46 | 47 | type IngressReference struct { 48 | Name string `json:"name"` 49 | } 50 | 51 | type EndpointGroupBindingStatus struct { 52 | // +optional 53 | // +kubebuilder:validation:Type:=array 54 | EndpointIds []string `json:"endpointIds"` 55 | // +kubebuilder:validation:Required 56 | // +kubebuilder:validation:Type:=integer 57 | // +kubebuilder:default=0 58 | ObservedGeneration int64 `json:"observedGeneration"` 59 | } 60 | 61 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 62 | // +kubebuilder:object:root=true 63 | // EndpointGroupBindingList is a list of EndpointGroupBinding 64 | type EndpointGroupBindingList struct { 65 | metav1.TypeMeta `json:",inline"` 66 | // +optional 67 | metav1.ListMeta `json:"metadata,omitempty"` 68 | 69 | Items []EndpointGroupBinding `json:"items"` 70 | } 71 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1/fake/fake_endpointgroupbinding.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | v1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis/endpointgroupbinding/v1alpha1" 23 | endpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1" 24 | gentype "k8s.io/client-go/gentype" 25 | ) 26 | 27 | // fakeEndpointGroupBindings implements EndpointGroupBindingInterface 28 | type fakeEndpointGroupBindings struct { 29 | *gentype.FakeClientWithList[*v1alpha1.EndpointGroupBinding, *v1alpha1.EndpointGroupBindingList] 30 | Fake *FakeOperatorV1alpha1 31 | } 32 | 33 | func newFakeEndpointGroupBindings(fake *FakeOperatorV1alpha1, namespace string) endpointgroupbindingv1alpha1.EndpointGroupBindingInterface { 34 | return &fakeEndpointGroupBindings{ 35 | gentype.NewFakeClientWithList[*v1alpha1.EndpointGroupBinding, *v1alpha1.EndpointGroupBindingList]( 36 | fake.Fake, 37 | namespace, 38 | v1alpha1.SchemeGroupVersion.WithResource("endpointgroupbindings"), 39 | v1alpha1.SchemeGroupVersion.WithKind("EndpointGroupBinding"), 40 | func() *v1alpha1.EndpointGroupBinding { return &v1alpha1.EndpointGroupBinding{} }, 41 | func() *v1alpha1.EndpointGroupBindingList { return &v1alpha1.EndpointGroupBindingList{} }, 42 | func(dst, src *v1alpha1.EndpointGroupBindingList) { dst.ListMeta = src.ListMeta }, 43 | func(list *v1alpha1.EndpointGroupBindingList) []*v1alpha1.EndpointGroupBinding { 44 | return gentype.ToPointerSlice(list.Items) 45 | }, 46 | func(list *v1alpha1.EndpointGroupBindingList, items []*v1alpha1.EndpointGroupBinding) { 47 | list.Items = gentype.FromPointerSlice(items) 48 | }, 49 | ), 50 | fake, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/leaderelection/leaderelection.go: -------------------------------------------------------------------------------- 1 | // https://github.com/kubernetes/client-go/blob/master/examples/leader-election/main.go 2 | package leaderelection 3 | 4 | import ( 5 | "context" 6 | "os" 7 | "time" 8 | 9 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/signals" 10 | 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/util/uuid" 13 | clientset "k8s.io/client-go/kubernetes" 14 | "k8s.io/client-go/rest" 15 | "k8s.io/client-go/tools/leaderelection" 16 | "k8s.io/client-go/tools/leaderelection/resourcelock" 17 | "k8s.io/klog/v2" 18 | ) 19 | 20 | type LeaderElection struct { 21 | name string 22 | namespace string 23 | } 24 | 25 | // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete 26 | // +kubebuilder:rbac:groups="",resources=configmaps/status,verbs=get;update;patch 27 | // +kubebuilder:rbac:groups="coordination.k8s.io",resources=leases,verbs=get;list;watch;create;update;patch;delete 28 | 29 | func NewLeaderElection(name, namespace string) *LeaderElection { 30 | return &LeaderElection{ 31 | name, 32 | namespace, 33 | } 34 | } 35 | 36 | func (le *LeaderElection) Run(ctx context.Context, cfg *rest.Config, run func(ctx context.Context, clientConfig *rest.Config, stopCh <-chan struct{})) error { 37 | stopCh := signals.SetupSignalHandler() 38 | 39 | client := clientset.NewForConfigOrDie(cfg) 40 | 41 | id := string(uuid.NewUUID()) 42 | klog.Infof("leader election id: %s", id) 43 | 44 | ctx, cancel := context.WithCancel(ctx) 45 | defer cancel() 46 | 47 | lock := &resourcelock.LeaseLock{ 48 | LeaseMeta: metav1.ObjectMeta{ 49 | Name: le.name, 50 | Namespace: le.namespace, 51 | }, 52 | Client: client.CoordinationV1(), 53 | LockConfig: resourcelock.ResourceLockConfig{ 54 | Identity: id, 55 | }, 56 | } 57 | 58 | leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ 59 | Lock: lock, 60 | ReleaseOnCancel: true, 61 | LeaseDuration: 60 * time.Second, 62 | RenewDeadline: 15 * time.Second, 63 | RetryPeriod: 5 * time.Second, 64 | Callbacks: leaderelection.LeaderCallbacks{ 65 | OnStartedLeading: func(ctx context.Context) { 66 | defer cancel() 67 | run(ctx, cfg, stopCh) 68 | os.Exit(0) 69 | }, 70 | OnStoppedLeading: func() { 71 | klog.Infof("leader lost: %s", id) 72 | os.Exit(0) 73 | }, 74 | OnNewLeader: func(identity string) { 75 | if identity == id { 76 | // I just got the lock 77 | return 78 | } 79 | klog.Infof("new leader elected: %s", identity) 80 | }, 81 | }, 82 | }) 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /local_e2e/pkg/fixtures/ingress.go: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis" 8 | corev1 "k8s.io/api/core/v1" 9 | networkingv1 "k8s.io/api/networking/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/util/intstr" 12 | utilpointer "k8s.io/utils/pointer" 13 | ) 14 | 15 | func NewALBIngress(ns, name, hostname string, port int) *networkingv1.Ingress { 16 | svc := newBackendService(ns, name) 17 | pt := networkingv1.PathTypePrefix 18 | listenPorts := "[{\"HTTPS\":" + strconv.Itoa(port) + "}]" 19 | 20 | return &networkingv1.Ingress{ 21 | ObjectMeta: metav1.ObjectMeta{ 22 | Name: name, 23 | Namespace: ns, 24 | Annotations: map[string]string{ 25 | apis.AWSGlobalAcceleratorManagedAnnotation: "true", 26 | apis.Route53HostnameAnnotation: hostname, 27 | "alb.ingress.kubernetes.io/scheme": "internet-facing", 28 | "alb.ingress.kubernetes.io/certificate-arn": os.Getenv("E2E_ACM_ARN"), 29 | "alb.ingress.kubernetes.io/listen-ports": listenPorts, 30 | }, 31 | }, 32 | Spec: networkingv1.IngressSpec{ 33 | IngressClassName: utilpointer.StringPtr("alb"), 34 | Rules: []networkingv1.IngressRule{ 35 | { 36 | IngressRuleValue: networkingv1.IngressRuleValue{ 37 | HTTP: &networkingv1.HTTPIngressRuleValue{ 38 | Paths: []networkingv1.HTTPIngressPath{ 39 | { 40 | Path: "/", 41 | PathType: &pt, 42 | Backend: networkingv1.IngressBackend{ 43 | Service: &networkingv1.IngressServiceBackend{ 44 | Name: svc.Name, 45 | Port: networkingv1.ServiceBackendPort{ 46 | Number: 80, 47 | }, 48 | }, 49 | }, 50 | }, 51 | }, 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | } 58 | } 59 | 60 | func newBackendService(ns, name string) *corev1.Service { 61 | return &corev1.Service{ 62 | ObjectMeta: metav1.ObjectMeta{ 63 | Name: name, 64 | Namespace: ns, 65 | }, 66 | Spec: corev1.ServiceSpec{ 67 | Ports: []corev1.ServicePort{ 68 | { 69 | Name: "http", 70 | Protocol: corev1.ProtocolTCP, 71 | Port: 80, 72 | TargetPort: intstr.IntOrString{ 73 | IntVal: 8080, 74 | }, 75 | }, 76 | { 77 | Name: "https", 78 | Protocol: corev1.ProtocolTCP, 79 | Port: 443, 80 | TargetPort: intstr.IntOrString{ 81 | IntVal: 6443, 82 | }, 83 | }, 84 | }, 85 | Selector: map[string]string{ 86 | "app": "h3poteto", 87 | }, 88 | Type: corev1.ServiceTypeNodePort, 89 | }, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | # Run tests for any PRs. 9 | pull_request: 10 | 11 | env: 12 | IMAGE_NAME: aws-global-accelerator 13 | KIND_VERSION: v0.29.0 14 | KUBECTL_VERSION: v1.33.3 15 | 16 | jobs: 17 | e2e-test: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | k8s-version: [1.32.5, 1.33.1] 23 | 24 | steps: 25 | - uses: actions/checkout@master 26 | - uses: actions/setup-go@v5 27 | with: 28 | go-version-file: "go.mod" 29 | - name: Install kind 30 | env: 31 | KIND_VERSION: ${{ env.KIND_VERSION }} 32 | BIN_DIR: ${{ github.workspace }}/tools/ 33 | run: | 34 | mkdir -p $BIN_DIR 35 | curl -sSLo "$BIN_DIR/kind" "https://github.com/kubernetes-sigs/kind/releases/download/$KIND_VERSION/kind-linux-amd64" 36 | chmod +x "$BIN_DIR/kind" 37 | echo "$BIN_DIR" >> "$GITHUB_PATH" 38 | - name: Install kubectl 39 | env: 40 | KUBECTL_VERSION: ${{ env.KUBECTL_VERSION }} 41 | BIN_DIR: ${{ github.workspace }}/tools/ 42 | run: | 43 | mkdir -p $BIN_DIR 44 | curl -sSLo "$BIN_DIR/kubectl" "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl" 45 | chmod +x "$BIN_DIR/kubectl" 46 | echo "$BIN_DIR" >> "$GITHUB_PATH" 47 | - name: Setup kind ${{ matrix.k8s-version }} 48 | env: 49 | K8S_VERSION: ${{ matrix.k8s-version }} 50 | run: | 51 | ./hack/kind-with-registry.sh 52 | - name: Info 53 | run: | 54 | kind version 55 | kubectl cluster-info 56 | kubectl version 57 | - name: Install cert-manager 58 | run: | 59 | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.0/cert-manager.crds.yaml 60 | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.0/cert-manager.yaml 61 | - name: Build docker image 62 | run: | 63 | IMAGE_ID=localhost:5000/$IMAGE_NAME 64 | SHA=${{ github.sha }} 65 | docker build . --file Dockerfile --tag $IMAGE_ID:$SHA 66 | docker push $IMAGE_ID:$SHA 67 | - name: Install ginkgo 68 | run: | 69 | go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo 70 | - name: Check nodes 71 | run: | 72 | kubectl get node 73 | - name: Testing 74 | run: | 75 | IMAGE_ID=localhost:5000/$IMAGE_NAME 76 | SHA=${{ github.sha }} 77 | export WEBHOOK_IMAGE=$IMAGE_ID:$SHA 78 | go mod download 79 | ginkgo -r ./e2e 80 | -------------------------------------------------------------------------------- /pkg/webhoook/endpointgroupbinding/validator.go: -------------------------------------------------------------------------------- 1 | package endpointgroupbinding 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | endpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis/endpointgroupbinding/v1alpha1" 9 | admissionv1 "k8s.io/api/admission/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | types "k8s.io/apimachinery/pkg/types" 12 | klog "k8s.io/klog/v2" 13 | ) 14 | 15 | func Validate(admission *admissionv1.AdmissionReview) *admissionv1.AdmissionReview { 16 | if admission.Request.Kind.Kind != "EndpointGroupBinding" { 17 | err := fmt.Errorf("%s is not supported", admission.Request.Kind.Kind) 18 | klog.Error(err) 19 | return reviewResponse(admission.Request.UID, false, 400, err.Error()) 20 | } 21 | 22 | if admission.Request.Operation != admissionv1.Update { 23 | klog.V(4).Info("Operation is not Update") 24 | return reviewResponse(admission.Request.UID, true, http.StatusOK, "") 25 | } 26 | 27 | if admission.Request.OldObject.Raw == nil { 28 | klog.V(4).Info("OldObject is nil") 29 | return reviewResponse(admission.Request.UID, true, http.StatusOK, "") 30 | } 31 | 32 | previous := endpointgroupbindingv1alpha1.EndpointGroupBinding{} 33 | new := endpointgroupbindingv1alpha1.EndpointGroupBinding{} 34 | if err := json.Unmarshal(admission.Request.OldObject.Raw, &previous); err != nil { 35 | klog.Error(err) 36 | return reviewResponse(admission.Request.UID, false, http.StatusInternalServerError, err.Error()) 37 | } 38 | 39 | if err := json.Unmarshal(admission.Request.Object.Raw, &new); err != nil { 40 | klog.Error(err) 41 | return reviewResponse(admission.Request.UID, false, http.StatusInternalServerError, err.Error()) 42 | } 43 | 44 | allowed, err := validate(&previous, &new) 45 | if err != nil { 46 | klog.Error(err) 47 | return reviewResponse(admission.Request.UID, false, http.StatusForbidden, err.Error()) 48 | } 49 | 50 | return reviewResponse(admission.Request.UID, allowed, http.StatusOK, "valid") 51 | } 52 | 53 | func validate(previous, new *endpointgroupbindingv1alpha1.EndpointGroupBinding) (bool, error) { 54 | if previous.Spec.EndpointGroupArn != new.Spec.EndpointGroupArn { 55 | return false, fmt.Errorf("Spec.EndpointGroupArn is immutable") 56 | } 57 | return true, nil 58 | } 59 | 60 | func reviewResponse(uid types.UID, allowed bool, code int32, reason string) *admissionv1.AdmissionReview { 61 | return &admissionv1.AdmissionReview{ 62 | TypeMeta: metav1.TypeMeta{ 63 | Kind: "AdmissionReview", 64 | APIVersion: "admission.k8s.io/v1", 65 | }, 66 | Response: &admissionv1.AdmissionResponse{ 67 | UID: uid, 68 | Allowed: allowed, 69 | Result: &metav1.Status{ 70 | Code: code, 71 | Message: reason, 72 | }, 73 | }, 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build run clean manifests controller-gen push 2 | 3 | # Get the currently used golang install path 4 | # Use ~/.local/bin for local development if it exists in PATH, otherwise use go bin 5 | ifneq (,$(and $(findstring $(HOME)/.local/bin,$(PATH)),$(wildcard $(HOME)/.local/bin))) 6 | GOBIN=$(HOME)/.local/bin 7 | else 8 | ifeq (,$(shell go env GOBIN)) 9 | GOBIN=$(shell go env GOPATH)/bin 10 | else 11 | GOBIN=$(shell go env GOBIN) 12 | endif 13 | endif 14 | 15 | CRD_OPTIONS ?= crd 16 | CODE_GENERATOR=${GOPATH}/src/k8s.io/code-generator 17 | CODE_GENERATOR_TAG=v0.33.3 18 | CONTROLLER_TOOLS_TAG=v0.18.0 19 | BRANCH := $(shell git branch --show-current) 20 | 21 | TARGETOS ?= linux 22 | TARGETARCH ?= amd64 23 | 24 | build: codegen manifests 25 | GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) CGO_ENABLED="0" \ 26 | go build -a -tags netgo -installsuffix netgo -ldflags \ 27 | " \ 28 | -extldflags '-static' \ 29 | -X github.com/h3poteto/aws-global-accelerator-controller/cmd.version=$(shell git describe --tag --abbrev=0) \ 30 | -X github.com/h3poteto/aws-global-accelerator-controller/cmd.revision=$(shell git rev-list -1 HEAD) \ 31 | -X github.com/h3poteto/aws-global-accelerator-controller/cmd.build=$(shell git describe --tags) \ 32 | " 33 | 34 | run: codegen manifests 35 | go run ./main.go controller --kubeconfig=${KUBECONFIG} 36 | 37 | install: manifests 38 | kubectl apply -f ./config/crd 39 | 40 | clean: 41 | rm -f $(GOBIN)/controller-gen 42 | rm -rf $(CODE_GENERATOR) 43 | 44 | codegen: code-generator 45 | @if command -v asdf >/dev/null 2>&1; then \ 46 | GOBIN=$$(asdf exec go env GOBIN) CODE_GENERATOR=${CODE_GENERATOR} hack/update-codegen.sh; \ 47 | else \ 48 | CODE_GENERATOR=${CODE_GENERATOR} hack/update-codegen.sh; \ 49 | fi 50 | 51 | code-generator: 52 | ifeq (, $(wildcard ${CODE_GENERATOR})) 53 | git clone https://github.com/kubernetes/code-generator.git ${CODE_GENERATOR} -b ${CODE_GENERATOR_TAG} --depth 1 54 | endif 55 | 56 | 57 | manifests: controller-gen 58 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=global-accelerator-manager-role webhook paths=./... output:crd:artifacts:config=./config/crd/ output:webhook:artifacts:config=./config/webhook/ 59 | 60 | 61 | controller-gen: 62 | ifeq (, $(shell which controller-gen)) 63 | @echo "controller-gen not found, downloading..." 64 | curl -L -o controller-gen https://github.com/kubernetes-sigs/controller-tools/releases/download/${CONTROLLER_TOOLS_TAG}/controller-gen-linux-amd64 65 | chmod +x controller-gen 66 | mv controller-gen $(GOBIN)/controller-gen 67 | CONTROLLER_GEN=$(GOBIN)/controller-gen 68 | else 69 | CONTROLLER_GEN=$(shell which controller-gen) 70 | endif 71 | 72 | push: 73 | docker build -f Dockerfile -t ghcr.io/h3poteto/aws-global-accelerator-controller:$(BRANCH) . 74 | docker push ghcr.io/h3poteto/aws-global-accelerator-controller:$(BRANCH) 75 | -------------------------------------------------------------------------------- /pkg/manager/manager.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | clientset "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned" 9 | ownInformers "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/informers/externalversions" 10 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/controller/endpointgroupbinding" 11 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/controller/globalaccelerator" 12 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/controller/route53" 13 | 14 | "k8s.io/client-go/informers" 15 | "k8s.io/client-go/kubernetes" 16 | "k8s.io/client-go/rest" 17 | "k8s.io/klog/v2" 18 | ) 19 | 20 | type manager struct{} 21 | 22 | type ControllerConfig struct { 23 | GlobalAccelerator *globalaccelerator.GlobalAcceleratorConfig 24 | Route53 *route53.Route53Config 25 | EndpointGroupBinding *endpointgroupbinding.EndpointGroupBindingConfig 26 | } 27 | 28 | func NewManager() *manager { 29 | return &manager{} 30 | } 31 | 32 | type InitFunc func(kubeClient kubernetes.Interface, ownClient clientset.Interface, informerFactory informers.SharedInformerFactory, ownInformers ownInformers.SharedInformerFactory, config *ControllerConfig, stopCh <-chan struct{}, done func()) (bool, error) 33 | 34 | func NewControllerInitializers() map[string]InitFunc { 35 | controllers := map[string]InitFunc{} 36 | controllers["global-accelerator-controller"] = startGlobalAcceleratorController 37 | controllers["route53-controller"] = startRoute53Controller 38 | controllers["endpoint-group-binding-controller"] = startEndpointGroupBindingController 39 | return controllers 40 | } 41 | 42 | func (m *manager) Run(ctx context.Context, clientConfig *rest.Config, config *ControllerConfig, stopCh <-chan struct{}) error { 43 | kubeClient, err := kubernetes.NewForConfig(clientConfig) 44 | if err != nil { 45 | return err 46 | } 47 | ownClient, err := clientset.NewForConfig(clientConfig) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | informerFactory := informers.NewSharedInformerFactory(kubeClient, time.Second*30) 53 | ownInformerFactory := ownInformers.NewSharedInformerFactory(ownClient, time.Second*30) 54 | 55 | controllers := NewControllerInitializers() 56 | var wg sync.WaitGroup 57 | for name, initFn := range controllers { 58 | wg.Add(1) 59 | klog.Infof("Starting %s", name) 60 | started, err := initFn(kubeClient, ownClient, informerFactory, ownInformerFactory, config, stopCh, wg.Done) 61 | if err != nil { 62 | return err 63 | } 64 | if !started { 65 | klog.Warningf("Skippping %s", name) 66 | continue 67 | } 68 | klog.Infof("Started %s", name) 69 | } 70 | 71 | go informerFactory.Start(stopCh) 72 | go ownInformerFactory.Start(stopCh) 73 | 74 | wg.Wait() 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/reconcile/reconcile.go: -------------------------------------------------------------------------------- 1 | package reconcile 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | pkgerrors "github.com/h3poteto/aws-global-accelerator-controller/pkg/errors" 9 | 10 | kerrors "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 13 | "k8s.io/client-go/util/workqueue" 14 | "k8s.io/klog/v2" 15 | ) 16 | 17 | type Result struct { 18 | Requeue bool 19 | RequeueAfter time.Duration 20 | } 21 | 22 | type KeyToObjFunc func(string) (runtime.Object, error) 23 | type ProcessDeleteFunc func(context.Context, string) (Result, error) 24 | type ProcessCreateOrUpdateFunc func(context.Context, runtime.Object) (Result, error) 25 | 26 | func ProcessNextWorkItem(workqueue workqueue.RateLimitingInterface, keyToObj KeyToObjFunc, processDelete ProcessDeleteFunc, processCreateOrUpdate ProcessCreateOrUpdateFunc) bool { 27 | obj, shutdown := workqueue.Get() 28 | 29 | if shutdown { 30 | return false 31 | } 32 | 33 | defer workqueue.Done(obj) 34 | 35 | err := reconcileHandler(obj, workqueue, keyToObj, processDelete, processCreateOrUpdate) 36 | if err != nil { 37 | utilruntime.HandleError(err) 38 | return true 39 | } 40 | 41 | return true 42 | } 43 | 44 | func reconcileHandler(req interface{}, workqueue workqueue.RateLimitingInterface, keyToObj KeyToObjFunc, processDelete ProcessDeleteFunc, processCreateOrUpdate ProcessCreateOrUpdateFunc) error { 45 | var key string 46 | var ok bool 47 | 48 | if key, ok = req.(string); !ok { 49 | workqueue.Forget(req) 50 | return fmt.Errorf("expected string in workqueue but got %#v", req) 51 | } 52 | startTime := time.Now() 53 | defer func() { 54 | klog.V(4).Infof("Finished syncing %q (%v)", key, time.Since(startTime)) 55 | }() 56 | 57 | ctx := context.Background() 58 | 59 | res := Result{} 60 | obj, err := keyToObj(key) 61 | switch { 62 | case kerrors.IsNotFound(err): 63 | res, err = processDelete(ctx, key) 64 | case err != nil: 65 | return fmt.Errorf("Unable to retrieve %q from store: %v", key, err) 66 | default: 67 | res, err = processCreateOrUpdate(ctx, obj.DeepCopyObject()) 68 | } 69 | 70 | switch { 71 | case err != nil: 72 | if pkgerrors.IsNoRetry(err) { 73 | return fmt.Errorf("error syncing %q: %s", key, err.Error()) 74 | } else { 75 | workqueue.AddRateLimited(req) 76 | return fmt.Errorf("error syncing %q, and requeued: %s", key, err.Error()) 77 | } 78 | 79 | case res.RequeueAfter > 0: 80 | workqueue.Forget(req) 81 | workqueue.AddAfter(req, res.RequeueAfter) 82 | klog.Infof("Successfully synced %q, but requeued after %v", key, res.RequeueAfter) 83 | case res.Requeue: 84 | workqueue.AddRateLimited(req) 85 | klog.Infof("Successfully synced %q, but requeued", key) 86 | default: 87 | workqueue.Forget(req) 88 | klog.Infof("Successfully synced %q", key) 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/cloudprovider/aws/load_balancer.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" 10 | elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" 11 | ) 12 | 13 | func (a *AWS) GetLoadBalancer(ctx context.Context, name string) (*elbv2types.LoadBalancer, error) { 14 | input := &elbv2.DescribeLoadBalancersInput{ 15 | Names: []string{ 16 | name, 17 | }, 18 | } 19 | res, err := a.lb.DescribeLoadBalancers(ctx, input) 20 | if err != nil { 21 | return nil, err 22 | } 23 | for _, lb := range res.LoadBalancers { 24 | if *lb.LoadBalancerName == name { 25 | return &lb, nil 26 | } 27 | } 28 | 29 | return nil, fmt.Errorf("Could not find LoadBalancer: %s", name) 30 | } 31 | 32 | func GetLBNameFromHostname(hostname string) (string, string, error) { 33 | albReg := regexp.MustCompile(`\.elb\.amazonaws\.com$`) 34 | nlbReg := regexp.MustCompile(`\.elb\..+\.amazonaws\.com$`) 35 | switch { 36 | case albReg.MatchString(hostname): 37 | return matchALBHostname(hostname) 38 | case nlbReg.MatchString(hostname): 39 | return matchNLBHostname(hostname) 40 | default: 41 | return "", "", fmt.Errorf("%s is not Elastic Load Balancer", hostname) 42 | } 43 | 44 | } 45 | 46 | func matchALBHostname(hostname string) (string, string, error) { 47 | slice := strings.Split(hostname, ".") 48 | subdomain := slice[0] 49 | region := slice[1] 50 | publicReg := regexp.MustCompile(`^internal-`) 51 | if publicReg.MatchString(subdomain) { 52 | name, err := internalALBName(subdomain) 53 | return name, region, err 54 | } else { 55 | name, err := publicALBName(subdomain) 56 | return name, region, err 57 | } 58 | } 59 | 60 | func internalALBName(subdomain string) (string, error) { 61 | nameReg := regexp.MustCompile(`^internal\-([\w\-]+)\-[\w]+$`) 62 | result := nameReg.FindAllStringSubmatch(subdomain, -1) 63 | if len(result) != 1 || len(result[0]) != 2 { 64 | return "", fmt.Errorf("Failed to parse subdomain for internal ALB: %s", subdomain) 65 | } 66 | return result[0][1], nil 67 | } 68 | 69 | func publicALBName(subdomain string) (string, error) { 70 | nameReg := regexp.MustCompile(`^([\w\-]+)\-[\w]+$`) 71 | result := nameReg.FindAllStringSubmatch(subdomain, -1) 72 | if len(result) != 1 || len(result[0]) != 2 { 73 | return "", fmt.Errorf("Failed to parse subdomain for public ALB: %s", subdomain) 74 | } 75 | return result[0][1], nil 76 | } 77 | 78 | func matchNLBHostname(hostname string) (string, string, error) { 79 | slice := strings.Split(hostname, ".") 80 | subdomain := slice[0] 81 | region := slice[2] 82 | name, err := nlbName(subdomain) 83 | return name, region, err 84 | } 85 | 86 | func nlbName(subdomain string) (string, error) { 87 | nameReg := regexp.MustCompile(`^([\w\-]+)\-[\w]+$`) 88 | result := nameReg.FindAllStringSubmatch(subdomain, -1) 89 | if len(result) != 1 || len(result[0]) != 2 { 90 | return "", fmt.Errorf("Failed to parse subdomain for NLB: %s", subdomain) 91 | } 92 | return result[0][1], nil 93 | } 94 | 95 | func GetRegionFromARN(arn string) string { 96 | slice := strings.Split(arn, ":") 97 | return slice[3] 98 | } 99 | -------------------------------------------------------------------------------- /local_e2e/pkg/fixtures/manager.go: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | rbacv1 "k8s.io/api/rbac/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | utilpointer "k8s.io/utils/pointer" 9 | ) 10 | 11 | const ( 12 | // This name must be same as config/rbac/role.yaml. 13 | clusterRoleName = "global-accelerator-manager-role" 14 | ) 15 | 16 | func NewManagerManifests(ns, name, image, clusterName string) (*corev1.ServiceAccount, *rbacv1.ClusterRoleBinding, *appsv1.Deployment) { 17 | return serviceAccount(ns, name), clusterRoleBinding(ns, name), deployment(ns, name, image, name, clusterName) 18 | } 19 | 20 | func serviceAccount(ns, name string) *corev1.ServiceAccount { 21 | return &corev1.ServiceAccount{ 22 | ObjectMeta: metav1.ObjectMeta{ 23 | Name: name, 24 | Namespace: ns, 25 | }, 26 | } 27 | } 28 | 29 | func clusterRoleBinding(ns, serviceAccountName string) *rbacv1.ClusterRoleBinding { 30 | return &rbacv1.ClusterRoleBinding{ 31 | ObjectMeta: metav1.ObjectMeta{ 32 | Name: "manager-role-binding", 33 | Namespace: ns, 34 | }, 35 | Subjects: []rbacv1.Subject{ 36 | { 37 | Kind: rbacv1.ServiceAccountKind, 38 | Name: serviceAccountName, 39 | Namespace: ns, 40 | }, 41 | }, 42 | RoleRef: rbacv1.RoleRef{ 43 | APIGroup: "rbac.authorization.k8s.io", 44 | Kind: "ClusterRole", 45 | Name: clusterRoleName, 46 | }, 47 | } 48 | } 49 | 50 | func deployment(ns, name, image, serviceAccountName, clusterName string) *appsv1.Deployment { 51 | return &appsv1.Deployment{ 52 | ObjectMeta: metav1.ObjectMeta{ 53 | Name: name, 54 | Namespace: ns, 55 | Labels: map[string]string{ 56 | "operator.h3poteto.dev": "control-plane", 57 | }, 58 | }, 59 | Spec: appsv1.DeploymentSpec{ 60 | Replicas: utilpointer.Int32Ptr(1), 61 | Selector: &metav1.LabelSelector{ 62 | MatchLabels: map[string]string{ 63 | "operator.h3poteto.dev": "control-plane", 64 | }, 65 | }, 66 | Template: corev1.PodTemplateSpec{ 67 | ObjectMeta: metav1.ObjectMeta{ 68 | Labels: map[string]string{ 69 | "operator.h3poteto.dev": "control-plane", 70 | }, 71 | }, 72 | Spec: corev1.PodSpec{ 73 | Containers: []corev1.Container{ 74 | { 75 | Name: "manager", 76 | Image: image, 77 | Args: []string{ 78 | "/aws-global-accelerator-controller", 79 | "controller", 80 | "--cluster-name=" + clusterName, 81 | "--v=4", 82 | }, 83 | Env: []corev1.EnvVar{ 84 | { 85 | Name: "POD_NAME", 86 | ValueFrom: &corev1.EnvVarSource{ 87 | FieldRef: &corev1.ObjectFieldSelector{ 88 | FieldPath: "metadata.name", 89 | }, 90 | }, 91 | }, 92 | { 93 | Name: "POD_NAMESPACE", 94 | ValueFrom: &corev1.EnvVarSource{ 95 | FieldRef: &corev1.ObjectFieldSelector{ 96 | FieldPath: "metadata.namespace", 97 | }, 98 | }, 99 | }, 100 | }, 101 | }, 102 | }, 103 | ServiceAccountName: serviceAccountName, 104 | }, 105 | }, 106 | }, 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /config/crd/operator.h3poteto.dev_endpointgroupbindings.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.18.0 7 | name: endpointgroupbindings.operator.h3poteto.dev 8 | spec: 9 | group: operator.h3poteto.dev 10 | names: 11 | kind: EndpointGroupBinding 12 | listKind: EndpointGroupBindingList 13 | plural: endpointgroupbindings 14 | singular: endpointgroupbinding 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .spec.endpointGroupArn 19 | name: EndpointGroupArn 20 | type: string 21 | - jsonPath: .status.endpointIds 22 | name: EndpointIds 23 | type: string 24 | - jsonPath: .metadata.creationTimestamp 25 | name: Age 26 | type: date 27 | name: v1alpha1 28 | schema: 29 | openAPIV3Schema: 30 | description: EndpointGroupBinding 31 | properties: 32 | apiVersion: 33 | description: |- 34 | APIVersion defines the versioned schema of this representation of an object. 35 | Servers should convert recognized schemas to the latest internal value, and 36 | may reject unrecognized values. 37 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 38 | type: string 39 | kind: 40 | description: |- 41 | Kind is a string value representing the REST resource this object represents. 42 | Servers may infer this from the endpoint the client submits requests to. 43 | Cannot be updated. 44 | In CamelCase. 45 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 46 | type: string 47 | metadata: 48 | type: object 49 | spec: 50 | properties: 51 | clientIPPreservation: 52 | default: false 53 | type: boolean 54 | endpointGroupArn: 55 | type: string 56 | ingressRef: 57 | properties: 58 | name: 59 | type: string 60 | required: 61 | - name 62 | type: object 63 | serviceRef: 64 | properties: 65 | name: 66 | type: string 67 | required: 68 | - name 69 | type: object 70 | weight: 71 | format: int32 72 | nullable: true 73 | type: integer 74 | required: 75 | - endpointGroupArn 76 | type: object 77 | status: 78 | properties: 79 | endpointIds: 80 | items: 81 | type: string 82 | type: array 83 | observedGeneration: 84 | default: 0 85 | format: int64 86 | type: integer 87 | required: 88 | - observedGeneration 89 | type: object 90 | type: object 91 | served: true 92 | storage: true 93 | subresources: 94 | status: {} 95 | -------------------------------------------------------------------------------- /cmd/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/controller/endpointgroupbinding" 8 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/controller/globalaccelerator" 9 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/controller/route53" 10 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/leaderelection" 11 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/manager" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | "k8s.io/client-go/rest" 15 | "k8s.io/client-go/tools/clientcmd" 16 | "k8s.io/klog/v2" 17 | ) 18 | 19 | type options struct { 20 | workers int 21 | clusterName string 22 | } 23 | 24 | func ControllerCmd() *cobra.Command { 25 | o := &options{} 26 | cmd := &cobra.Command{ 27 | Use: "controller", 28 | Short: "Start controller", 29 | Run: o.run, 30 | } 31 | flags := cmd.Flags() 32 | flags.IntVarP(&o.workers, "workers", "w", 1, "Concurrent workers number for controller.") 33 | flags.StringVarP(&o.clusterName, "cluster-name", "c", "default", "Owner cluster name which is used in resource tags.") 34 | 35 | cmd.PersistentFlags().String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") 36 | cmd.PersistentFlags().String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") 37 | _ = viper.BindPFlag("kubeconfig", cmd.PersistentFlags().Lookup("kubeconfig")) 38 | _ = viper.BindPFlag("master", cmd.PersistentFlags().Lookup("master")) 39 | 40 | return cmd 41 | } 42 | 43 | func (o *options) run(cmd *cobra.Command, args []string) { 44 | kubeconfig, masterURL := controllerConfig() 45 | if kubeconfig != "" { 46 | klog.Infof("Using kubeconfig: %s", kubeconfig) 47 | } else { 48 | klog.Info("Using in-cluster config") 49 | } 50 | cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig) 51 | if err != nil { 52 | klog.Fatalf("Error building rest config: %s", err.Error()) 53 | } 54 | 55 | ns := os.Getenv("POD_NAMESPACE") 56 | if ns == "" { 57 | ns = "default" 58 | } 59 | config := manager.ControllerConfig{ 60 | GlobalAccelerator: &globalaccelerator.GlobalAcceleratorConfig{ 61 | Workers: o.workers, 62 | ClusterName: o.clusterName, 63 | }, 64 | Route53: &route53.Route53Config{ 65 | Workers: o.workers, 66 | ClusterName: o.clusterName, 67 | }, 68 | EndpointGroupBinding: &endpointgroupbinding.EndpointGroupBindingConfig{ 69 | Workers: o.workers, 70 | }, 71 | } 72 | 73 | le := leaderelection.NewLeaderElection("aws-global-accelerator-controller", ns) 74 | ctx := context.Background() 75 | err = le.Run(ctx, cfg, func(ctx context.Context, clientConfig *rest.Config, stopCh <-chan struct{}) { 76 | m := manager.NewManager() 77 | if err := m.Run(ctx, clientConfig, &config, stopCh); err != nil { 78 | klog.Fatalf("Error running controller: %v", err) 79 | } 80 | }) 81 | klog.Fatalf("Error starting controller: %s", err.Error()) 82 | } 83 | 84 | func controllerConfig() (string, string) { 85 | kubeconfig := viper.GetString("kubeconfig") 86 | if kubeconfig == "" { 87 | kubeconfig = os.Getenv("KUBECONFIG") 88 | if kubeconfig == "" { 89 | kubeconfig = os.ExpandEnv("$HOME/.kube/config") 90 | if _, err := os.Stat(kubeconfig); err != nil { 91 | klog.Error(err) 92 | kubeconfig = "" 93 | } 94 | } 95 | } 96 | master := viper.GetString("master") 97 | return kubeconfig, master 98 | } 99 | -------------------------------------------------------------------------------- /pkg/client/listers/endpointgroupbinding/v1alpha1/endpointgroupbinding.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | endpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis/endpointgroupbinding/v1alpha1" 23 | labels "k8s.io/apimachinery/pkg/labels" 24 | listers "k8s.io/client-go/listers" 25 | cache "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | // EndpointGroupBindingLister helps list EndpointGroupBindings. 29 | // All objects returned here must be treated as read-only. 30 | type EndpointGroupBindingLister interface { 31 | // List lists all EndpointGroupBindings in the indexer. 32 | // Objects returned here must be treated as read-only. 33 | List(selector labels.Selector) (ret []*endpointgroupbindingv1alpha1.EndpointGroupBinding, err error) 34 | // EndpointGroupBindings returns an object that can list and get EndpointGroupBindings. 35 | EndpointGroupBindings(namespace string) EndpointGroupBindingNamespaceLister 36 | EndpointGroupBindingListerExpansion 37 | } 38 | 39 | // endpointGroupBindingLister implements the EndpointGroupBindingLister interface. 40 | type endpointGroupBindingLister struct { 41 | listers.ResourceIndexer[*endpointgroupbindingv1alpha1.EndpointGroupBinding] 42 | } 43 | 44 | // NewEndpointGroupBindingLister returns a new EndpointGroupBindingLister. 45 | func NewEndpointGroupBindingLister(indexer cache.Indexer) EndpointGroupBindingLister { 46 | return &endpointGroupBindingLister{listers.New[*endpointgroupbindingv1alpha1.EndpointGroupBinding](indexer, endpointgroupbindingv1alpha1.Resource("endpointgroupbinding"))} 47 | } 48 | 49 | // EndpointGroupBindings returns an object that can list and get EndpointGroupBindings. 50 | func (s *endpointGroupBindingLister) EndpointGroupBindings(namespace string) EndpointGroupBindingNamespaceLister { 51 | return endpointGroupBindingNamespaceLister{listers.NewNamespaced[*endpointgroupbindingv1alpha1.EndpointGroupBinding](s.ResourceIndexer, namespace)} 52 | } 53 | 54 | // EndpointGroupBindingNamespaceLister helps list and get EndpointGroupBindings. 55 | // All objects returned here must be treated as read-only. 56 | type EndpointGroupBindingNamespaceLister interface { 57 | // List lists all EndpointGroupBindings in the indexer for a given namespace. 58 | // Objects returned here must be treated as read-only. 59 | List(selector labels.Selector) (ret []*endpointgroupbindingv1alpha1.EndpointGroupBinding, err error) 60 | // Get retrieves the EndpointGroupBinding from the indexer for a given namespace and name. 61 | // Objects returned here must be treated as read-only. 62 | Get(name string) (*endpointgroupbindingv1alpha1.EndpointGroupBinding, error) 63 | EndpointGroupBindingNamespaceListerExpansion 64 | } 65 | 66 | // endpointGroupBindingNamespaceLister implements the EndpointGroupBindingNamespaceLister 67 | // interface. 68 | type endpointGroupBindingNamespaceLister struct { 69 | listers.ResourceIndexer[*endpointgroupbindingv1alpha1.EndpointGroupBinding] 70 | } 71 | -------------------------------------------------------------------------------- /pkg/controller/route53/ingress.go: -------------------------------------------------------------------------------- 1 | package route53 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis" 8 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/cloudprovider" 9 | cloudaws "github.com/h3poteto/aws-global-accelerator-controller/pkg/cloudprovider/aws" 10 | pkgerrors "github.com/h3poteto/aws-global-accelerator-controller/pkg/errors" 11 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/reconcile" 12 | 13 | corev1 "k8s.io/api/core/v1" 14 | networkingv1 "k8s.io/api/networking/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/client-go/tools/cache" 17 | "k8s.io/klog/v2" 18 | ) 19 | 20 | func (c *Route53Controller) processIngressDelete(ctx context.Context, key string) (reconcile.Result, error) { 21 | klog.Infof("%v has been deleted", key) 22 | ns, name, err := cache.SplitMetaNamespaceKey(key) 23 | if err != nil { 24 | return reconcile.Result{}, pkgerrors.NewNoRetryErrorf("invalid resource key: %s", key) 25 | } 26 | cloud, err := cloudaws.NewAWS("us-west-2") 27 | if err != nil { 28 | klog.Error(err) 29 | return reconcile.Result{}, err 30 | } 31 | err = cloud.CleanupRecordSet(ctx, c.clusterName, "ingress", ns, name) 32 | if err != nil { 33 | klog.Error(err) 34 | return reconcile.Result{}, err 35 | } 36 | return reconcile.Result{}, nil 37 | 38 | } 39 | 40 | func (c *Route53Controller) processIngressCreateOrUpdate(ctx context.Context, obj runtime.Object) (reconcile.Result, error) { 41 | ingress, ok := obj.(*networkingv1.Ingress) 42 | if !ok { 43 | return reconcile.Result{}, pkgerrors.NewNoRetryErrorf("object is not Ingress, it is %T", obj) 44 | } 45 | 46 | hostname, ok := ingress.Annotations[apis.Route53HostnameAnnotation] 47 | if !ok { 48 | cloud, err := cloudaws.NewAWS("us-west-2") 49 | if err != nil { 50 | klog.Error(err) 51 | return reconcile.Result{}, err 52 | } 53 | err = cloud.CleanupRecordSet(ctx, c.clusterName, "ingress", ingress.Namespace, ingress.Name) 54 | if err != nil { 55 | klog.Error(err) 56 | return reconcile.Result{}, err 57 | } 58 | klog.Infof("Delete route53 records for Ingress %s/%s", ingress.Namespace, ingress.Name) 59 | c.recorder.Event(ingress, corev1.EventTypeNormal, "Route53RecordDeleted", "Route53 record sets are deleted") 60 | return reconcile.Result{}, nil 61 | } 62 | 63 | hostnames := strings.Split(hostname, ",") 64 | 65 | for i := range ingress.Status.LoadBalancer.Ingress { 66 | lbIngress := ingress.Status.LoadBalancer.Ingress[i] 67 | provider, err := cloudprovider.DetectCloudProvider(lbIngress.Hostname) 68 | if err != nil { 69 | klog.Error(err) 70 | continue 71 | } 72 | switch provider { 73 | case "aws": 74 | _, region, err := cloudaws.GetLBNameFromHostname(lbIngress.Hostname) 75 | if err != nil { 76 | klog.Error(err) 77 | return reconcile.Result{}, err 78 | } 79 | cloud, err := cloudaws.NewAWS(region) 80 | if err != nil { 81 | klog.Error(err) 82 | return reconcile.Result{}, err 83 | } 84 | created, retryAfter, err := cloud.EnsureRoute53ForIngress(ctx, ingress, &lbIngress, hostnames, c.clusterName) 85 | if err != nil { 86 | return reconcile.Result{}, err 87 | } 88 | if retryAfter > 0 { 89 | return reconcile.Result{ 90 | Requeue: true, 91 | RequeueAfter: retryAfter, 92 | }, nil 93 | } 94 | if created { 95 | c.recorder.Eventf(ingress, corev1.EventTypeNormal, "Route53RecordCreated", "Route53 record set is created: %v", hostnames) 96 | } 97 | default: 98 | klog.Warningf("Not implemented for %s", provider) 99 | continue 100 | } 101 | } 102 | 103 | return reconcile.Result{}, nil 104 | } 105 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | clientset "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned" 23 | operatorv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1" 24 | fakeoperatorv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1/fake" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/watch" 28 | "k8s.io/client-go/discovery" 29 | fakediscovery "k8s.io/client-go/discovery/fake" 30 | "k8s.io/client-go/testing" 31 | ) 32 | 33 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 34 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 35 | // without applying any field management, validations and/or defaults. It shouldn't be considered a replacement 36 | // for a real clientset and is mostly useful in simple unit tests. 37 | // 38 | // DEPRECATED: NewClientset replaces this with support for field management, which significantly improves 39 | // server side apply testing. NewClientset is only available when apply configurations are generated (e.g. 40 | // via --with-applyconfig). 41 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 42 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 43 | for _, obj := range objects { 44 | if err := o.Add(obj); err != nil { 45 | panic(err) 46 | } 47 | } 48 | 49 | cs := &Clientset{tracker: o} 50 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 51 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 52 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 53 | var opts metav1.ListOptions 54 | if watchActcion, ok := action.(testing.WatchActionImpl); ok { 55 | opts = watchActcion.ListOptions 56 | } 57 | gvr := action.GetResource() 58 | ns := action.GetNamespace() 59 | watch, err := o.Watch(gvr, ns, opts) 60 | if err != nil { 61 | return false, nil, err 62 | } 63 | return true, watch, nil 64 | }) 65 | 66 | return cs 67 | } 68 | 69 | // Clientset implements clientset.Interface. Meant to be embedded into a 70 | // struct to get a default implementation. This makes faking out just the method 71 | // you want to test easier. 72 | type Clientset struct { 73 | testing.Fake 74 | discovery *fakediscovery.FakeDiscovery 75 | tracker testing.ObjectTracker 76 | } 77 | 78 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 79 | return c.discovery 80 | } 81 | 82 | func (c *Clientset) Tracker() testing.ObjectTracker { 83 | return c.tracker 84 | } 85 | 86 | var ( 87 | _ clientset.Interface = &Clientset{} 88 | _ testing.FakeClient = &Clientset{} 89 | ) 90 | 91 | // OperatorV1alpha1 retrieves the OperatorV1alpha1Client 92 | func (c *Clientset) OperatorV1alpha1() operatorv1alpha1.OperatorV1alpha1Interface { 93 | return &fakeoperatorv1alpha1.FakeOperatorV1alpha1{Fake: &c.Fake} 94 | } 95 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1/endpointgroupbinding_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | http "net/http" 23 | 24 | endpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis/endpointgroupbinding/v1alpha1" 25 | scheme "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned/scheme" 26 | rest "k8s.io/client-go/rest" 27 | ) 28 | 29 | type OperatorV1alpha1Interface interface { 30 | RESTClient() rest.Interface 31 | EndpointGroupBindingsGetter 32 | } 33 | 34 | // OperatorV1alpha1Client is used to interact with features provided by the operator.h3poteto.dev group. 35 | type OperatorV1alpha1Client struct { 36 | restClient rest.Interface 37 | } 38 | 39 | func (c *OperatorV1alpha1Client) EndpointGroupBindings(namespace string) EndpointGroupBindingInterface { 40 | return newEndpointGroupBindings(c, namespace) 41 | } 42 | 43 | // NewForConfig creates a new OperatorV1alpha1Client for the given config. 44 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 45 | // where httpClient was generated with rest.HTTPClientFor(c). 46 | func NewForConfig(c *rest.Config) (*OperatorV1alpha1Client, error) { 47 | config := *c 48 | setConfigDefaults(&config) 49 | httpClient, err := rest.HTTPClientFor(&config) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return NewForConfigAndClient(&config, httpClient) 54 | } 55 | 56 | // NewForConfigAndClient creates a new OperatorV1alpha1Client for the given config and http client. 57 | // Note the http client provided takes precedence over the configured transport values. 58 | func NewForConfigAndClient(c *rest.Config, h *http.Client) (*OperatorV1alpha1Client, error) { 59 | config := *c 60 | setConfigDefaults(&config) 61 | client, err := rest.RESTClientForConfigAndClient(&config, h) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return &OperatorV1alpha1Client{client}, nil 66 | } 67 | 68 | // NewForConfigOrDie creates a new OperatorV1alpha1Client for the given config and 69 | // panics if there is an error in the config. 70 | func NewForConfigOrDie(c *rest.Config) *OperatorV1alpha1Client { 71 | client, err := NewForConfig(c) 72 | if err != nil { 73 | panic(err) 74 | } 75 | return client 76 | } 77 | 78 | // New creates a new OperatorV1alpha1Client for the given RESTClient. 79 | func New(c rest.Interface) *OperatorV1alpha1Client { 80 | return &OperatorV1alpha1Client{c} 81 | } 82 | 83 | func setConfigDefaults(config *rest.Config) { 84 | gv := endpointgroupbindingv1alpha1.SchemeGroupVersion 85 | config.GroupVersion = &gv 86 | config.APIPath = "/apis" 87 | config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() 88 | 89 | if config.UserAgent == "" { 90 | config.UserAgent = rest.DefaultKubernetesUserAgent() 91 | } 92 | } 93 | 94 | // RESTClient returns a RESTClient that is used to communicate 95 | // with API server by this client implementation. 96 | func (c *OperatorV1alpha1Client) RESTClient() rest.Interface { 97 | if c == nil { 98 | return nil 99 | } 100 | return c.restClient 101 | } 102 | -------------------------------------------------------------------------------- /pkg/controller/route53/service.go: -------------------------------------------------------------------------------- 1 | package route53 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis" 8 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/cloudprovider" 9 | cloudaws "github.com/h3poteto/aws-global-accelerator-controller/pkg/cloudprovider/aws" 10 | pkgerrors "github.com/h3poteto/aws-global-accelerator-controller/pkg/errors" 11 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/reconcile" 12 | 13 | corev1 "k8s.io/api/core/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/client-go/tools/cache" 16 | "k8s.io/klog/v2" 17 | ) 18 | 19 | func wasLoadBalancerService(svc *corev1.Service) bool { 20 | if svc.Spec.Type == corev1.ServiceTypeLoadBalancer { 21 | if _, ok := svc.Annotations[apis.AWSLoadBalancerTypeAnnotation]; ok || svc.Spec.LoadBalancerClass != nil { 22 | return true 23 | } 24 | } 25 | 26 | return false 27 | } 28 | 29 | func (c *Route53Controller) processServiceDelete(ctx context.Context, key string) (reconcile.Result, error) { 30 | klog.Infof("%v has been deleted", key) 31 | ns, name, err := cache.SplitMetaNamespaceKey(key) 32 | if err != nil { 33 | return reconcile.Result{}, pkgerrors.NewNoRetryErrorf("invalid resource key: %s", key) 34 | } 35 | cloud, err := cloudaws.NewAWS("us-west-2") 36 | if err != nil { 37 | klog.Error(err) 38 | return reconcile.Result{}, err 39 | } 40 | err = cloud.CleanupRecordSet(ctx, c.clusterName, "service", ns, name) 41 | if err != nil { 42 | klog.Error(err) 43 | return reconcile.Result{}, err 44 | } 45 | return reconcile.Result{}, nil 46 | } 47 | 48 | func (c *Route53Controller) processServiceCreateOrUpdate(ctx context.Context, obj runtime.Object) (reconcile.Result, error) { 49 | svc, ok := obj.(*corev1.Service) 50 | if !ok { 51 | return reconcile.Result{}, pkgerrors.NewNoRetryErrorf("object is not Service, it is %T", obj) 52 | } 53 | 54 | hostname, ok := svc.Annotations[apis.Route53HostnameAnnotation] 55 | if !ok { 56 | cloud, err := cloudaws.NewAWS("us-west-2") 57 | if err != nil { 58 | klog.Error(err) 59 | return reconcile.Result{}, err 60 | } 61 | err = cloud.CleanupRecordSet(ctx, c.clusterName, "service", svc.Namespace, svc.Name) 62 | if err != nil { 63 | klog.Error(err) 64 | return reconcile.Result{}, err 65 | } 66 | klog.Infof("Delete route53 records for Service %s/%s", svc.Namespace, svc.Name) 67 | c.recorder.Event(svc, corev1.EventTypeNormal, "Route53RecordDeleted", "Route53 record sets are deleted") 68 | return reconcile.Result{}, nil 69 | } 70 | 71 | hostnames := strings.Split(hostname, ",") 72 | 73 | for i := range svc.Status.LoadBalancer.Ingress { 74 | lbIngress := svc.Status.LoadBalancer.Ingress[i] 75 | provider, err := cloudprovider.DetectCloudProvider(lbIngress.Hostname) 76 | if err != nil { 77 | klog.Error(err) 78 | continue 79 | } 80 | switch provider { 81 | case "aws": 82 | _, region, err := cloudaws.GetLBNameFromHostname(lbIngress.Hostname) 83 | if err != nil { 84 | klog.Error(err) 85 | return reconcile.Result{}, err 86 | } 87 | cloud, err := cloudaws.NewAWS(region) 88 | if err != nil { 89 | klog.Error(err) 90 | return reconcile.Result{}, err 91 | } 92 | created, retryAfter, err := cloud.EnsureRoute53ForService(ctx, svc, &lbIngress, hostnames, c.clusterName) 93 | if err != nil { 94 | return reconcile.Result{}, err 95 | } 96 | if retryAfter > 0 { 97 | return reconcile.Result{ 98 | Requeue: true, 99 | RequeueAfter: retryAfter, 100 | }, nil 101 | } 102 | if created { 103 | c.recorder.Eventf(svc, corev1.EventTypeNormal, "Route53RecourdCreated", "Route53 record set is created: %v", hostnames) 104 | } 105 | default: 106 | klog.Warningf("Not impelmented for %s", provider) 107 | continue 108 | } 109 | } 110 | return reconcile.Result{}, nil 111 | } 112 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1/endpointgroupbinding.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | context "context" 23 | 24 | endpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis/endpointgroupbinding/v1alpha1" 25 | scheme "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned/scheme" 26 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | types "k8s.io/apimachinery/pkg/types" 28 | watch "k8s.io/apimachinery/pkg/watch" 29 | gentype "k8s.io/client-go/gentype" 30 | ) 31 | 32 | // EndpointGroupBindingsGetter has a method to return a EndpointGroupBindingInterface. 33 | // A group's client should implement this interface. 34 | type EndpointGroupBindingsGetter interface { 35 | EndpointGroupBindings(namespace string) EndpointGroupBindingInterface 36 | } 37 | 38 | // EndpointGroupBindingInterface has methods to work with EndpointGroupBinding resources. 39 | type EndpointGroupBindingInterface interface { 40 | Create(ctx context.Context, endpointGroupBinding *endpointgroupbindingv1alpha1.EndpointGroupBinding, opts v1.CreateOptions) (*endpointgroupbindingv1alpha1.EndpointGroupBinding, error) 41 | Update(ctx context.Context, endpointGroupBinding *endpointgroupbindingv1alpha1.EndpointGroupBinding, opts v1.UpdateOptions) (*endpointgroupbindingv1alpha1.EndpointGroupBinding, error) 42 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 43 | UpdateStatus(ctx context.Context, endpointGroupBinding *endpointgroupbindingv1alpha1.EndpointGroupBinding, opts v1.UpdateOptions) (*endpointgroupbindingv1alpha1.EndpointGroupBinding, error) 44 | Delete(ctx context.Context, name string, opts v1.DeleteOptions) error 45 | DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error 46 | Get(ctx context.Context, name string, opts v1.GetOptions) (*endpointgroupbindingv1alpha1.EndpointGroupBinding, error) 47 | List(ctx context.Context, opts v1.ListOptions) (*endpointgroupbindingv1alpha1.EndpointGroupBindingList, error) 48 | Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) 49 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *endpointgroupbindingv1alpha1.EndpointGroupBinding, err error) 50 | EndpointGroupBindingExpansion 51 | } 52 | 53 | // endpointGroupBindings implements EndpointGroupBindingInterface 54 | type endpointGroupBindings struct { 55 | *gentype.ClientWithList[*endpointgroupbindingv1alpha1.EndpointGroupBinding, *endpointgroupbindingv1alpha1.EndpointGroupBindingList] 56 | } 57 | 58 | // newEndpointGroupBindings returns a EndpointGroupBindings 59 | func newEndpointGroupBindings(c *OperatorV1alpha1Client, namespace string) *endpointGroupBindings { 60 | return &endpointGroupBindings{ 61 | gentype.NewClientWithList[*endpointgroupbindingv1alpha1.EndpointGroupBinding, *endpointgroupbindingv1alpha1.EndpointGroupBindingList]( 62 | "endpointgroupbindings", 63 | c.RESTClient(), 64 | scheme.ParameterCodec, 65 | namespace, 66 | func() *endpointgroupbindingv1alpha1.EndpointGroupBinding { 67 | return &endpointgroupbindingv1alpha1.EndpointGroupBinding{} 68 | }, 69 | func() *endpointgroupbindingv1alpha1.EndpointGroupBindingList { 70 | return &endpointgroupbindingv1alpha1.EndpointGroupBindingList{} 71 | }, 72 | ), 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /local_e2e/pkg/fixtures/manifests.go: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | 12 | "gopkg.in/yaml.v3" 13 | "k8s.io/apimachinery/pkg/api/meta" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | serializeryaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 18 | "k8s.io/apimachinery/pkg/types" 19 | utilyaml "k8s.io/apimachinery/pkg/util/yaml" 20 | "k8s.io/client-go/discovery" 21 | memory "k8s.io/client-go/discovery/cached" 22 | "k8s.io/client-go/dynamic" 23 | "k8s.io/client-go/rest" 24 | "k8s.io/client-go/restmapper" 25 | ) 26 | 27 | func ApplyClusterRole(ctx context.Context, cfg *rest.Config) error { 28 | p, err := os.Getwd() 29 | if err != nil { 30 | return err 31 | } 32 | path := filepath.Join(p, "../config/rbac/role.yaml") 33 | buf, err := os.ReadFile(path) 34 | if err != nil { 35 | return err 36 | } 37 | return apply(ctx, cfg, buf) 38 | } 39 | 40 | func DeleteClusterRole(ctx context.Context, cfg *rest.Config) error { 41 | p, err := os.Getwd() 42 | if err != nil { 43 | return err 44 | } 45 | path := filepath.Join(p, "../config/rbac/role.yaml") 46 | buf, err := os.ReadFile(path) 47 | if err != nil { 48 | return err 49 | } 50 | return delete(ctx, cfg, buf) 51 | } 52 | 53 | func apply(ctx context.Context, cfg *rest.Config, data []byte) error { 54 | return restAction(ctx, cfg, data, func(ctx context.Context, dr dynamic.ResourceInterface, obj *unstructured.Unstructured, data []byte) error { 55 | _, err := dr.Patch(ctx, obj.GetName(), types.ApplyPatchType, data, metav1.PatchOptions{ 56 | FieldManager: "e2e", 57 | }) 58 | return err 59 | }) 60 | } 61 | 62 | func delete(ctx context.Context, cfg *rest.Config, data []byte) error { 63 | return restAction(ctx, cfg, data, func(ctx context.Context, dr dynamic.ResourceInterface, obj *unstructured.Unstructured, data []byte) error { 64 | err := dr.Delete(ctx, obj.GetName(), metav1.DeleteOptions{}) 65 | return err 66 | }) 67 | } 68 | 69 | type actionFunc func(ctx context.Context, dr dynamic.ResourceInterface, obj *unstructured.Unstructured, data []byte) error 70 | 71 | // https://ymmt2005.hatenablog.com/entry/2020/04/14/An_example_of_using_dynamic_client_of_k8s.io/client-go 72 | func restAction(ctx context.Context, cfg *rest.Config, data []byte, action actionFunc) error { 73 | dc, err := discovery.NewDiscoveryClientForConfig(cfg) 74 | if err != nil { 75 | return err 76 | } 77 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc)) 78 | 79 | dyn, err := dynamic.NewForConfig(cfg) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | multidocReader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data))) 85 | 86 | for { 87 | buf, err := multidocReader.Read() 88 | if err != nil { 89 | if err == io.EOF { 90 | return nil 91 | } 92 | return err 93 | } 94 | 95 | var typeMeta runtime.TypeMeta 96 | if err := yaml.Unmarshal(buf, &typeMeta); err != nil { 97 | continue 98 | } 99 | if typeMeta.Kind == "" { 100 | continue 101 | } 102 | 103 | obj := &unstructured.Unstructured{} 104 | _, gvk, err := serializeryaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).Decode(buf, nil, obj) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | var dr dynamic.ResourceInterface 115 | if mapping.Scope.Name() == meta.RESTScopeNameNamespace { 116 | dr = dyn.Resource(mapping.Resource).Namespace(obj.GetNamespace()) 117 | } else { 118 | dr = dyn.Resource(mapping.Resource) 119 | } 120 | 121 | data, err := json.Marshal(obj) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | if err := action(ctx, dr, obj, data); err != nil { 127 | return err 128 | } 129 | 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/h3poteto/aws-global-accelerator-controller 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.5 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go-v2 v1.37.2 9 | github.com/aws/aws-sdk-go-v2/config v1.30.3 10 | github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.48.0 11 | github.com/aws/aws-sdk-go-v2/service/globalaccelerator v1.32.0 12 | github.com/aws/aws-sdk-go-v2/service/route53 v1.55.0 13 | github.com/aws/smithy-go v1.22.5 14 | github.com/onsi/ginkgo/v2 v2.25.3 15 | github.com/onsi/gomega v1.38.2 16 | github.com/spf13/cobra v1.9.1 17 | github.com/spf13/viper v1.20.1 18 | github.com/stretchr/testify v1.11.1 19 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 20 | gopkg.in/yaml.v3 v3.0.1 21 | k8s.io/api v0.33.3 22 | k8s.io/apimachinery v0.33.3 23 | k8s.io/client-go v0.33.3 24 | k8s.io/klog/v2 v2.130.1 25 | k8s.io/kubectl v0.33.3 26 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 27 | ) 28 | 29 | require ( 30 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 31 | github.com/aws/aws-sdk-go-v2/credentials v1.18.3 // indirect 32 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2 // indirect 33 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 // indirect 34 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 // indirect 35 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 36 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect 37 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2 // indirect 38 | github.com/aws/aws-sdk-go-v2/service/sso v1.27.0 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 // indirect 40 | github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 // indirect 41 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 42 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 43 | github.com/fsnotify/fsnotify v1.8.0 // indirect 44 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 45 | github.com/go-logr/logr v1.4.3 // indirect 46 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 47 | github.com/go-openapi/jsonreference v0.20.2 // indirect 48 | github.com/go-openapi/swag v0.23.0 // indirect 49 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 50 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 51 | github.com/gogo/protobuf v1.3.2 // indirect 52 | github.com/google/gnostic-models v0.6.9 // indirect 53 | github.com/google/go-cmp v0.7.0 // indirect 54 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 55 | github.com/google/uuid v1.6.0 // indirect 56 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 57 | github.com/josharian/intern v1.0.0 // indirect 58 | github.com/json-iterator/go v1.1.12 // indirect 59 | github.com/mailru/easyjson v0.7.7 // indirect 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 61 | github.com/modern-go/reflect2 v1.0.2 // indirect 62 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 63 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 64 | github.com/pkg/errors v0.9.1 // indirect 65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 66 | github.com/sagikazarmark/locafero v0.7.0 // indirect 67 | github.com/sourcegraph/conc v0.3.0 // indirect 68 | github.com/spf13/afero v1.12.0 // indirect 69 | github.com/spf13/cast v1.7.1 // indirect 70 | github.com/spf13/pflag v1.0.6 // indirect 71 | github.com/subosito/gotenv v1.6.0 // indirect 72 | github.com/x448/float16 v0.8.4 // indirect 73 | go.uber.org/atomic v1.9.0 // indirect 74 | go.uber.org/automaxprocs v1.6.0 // indirect 75 | go.uber.org/multierr v1.9.0 // indirect 76 | go.yaml.in/yaml/v3 v3.0.4 // indirect 77 | golang.org/x/net v0.43.0 // indirect 78 | golang.org/x/oauth2 v0.27.0 // indirect 79 | golang.org/x/sys v0.35.0 // indirect 80 | golang.org/x/term v0.34.0 // indirect 81 | golang.org/x/text v0.28.0 // indirect 82 | golang.org/x/time v0.9.0 // indirect 83 | golang.org/x/tools v0.36.0 // indirect 84 | google.golang.org/protobuf v1.36.7 // indirect 85 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 86 | gopkg.in/inf.v0 v0.9.1 // indirect 87 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 88 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 89 | sigs.k8s.io/randfill v1.0.0 // indirect 90 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 91 | sigs.k8s.io/yaml v1.4.0 // indirect 92 | ) 93 | -------------------------------------------------------------------------------- /e2e/pkg/util/manifests.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/h3poteto/aws-global-accelerator-controller/e2e/pkg/templates" 13 | "gopkg.in/yaml.v3" 14 | "k8s.io/apimachinery/pkg/api/meta" 15 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | serializeryaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 19 | "k8s.io/apimachinery/pkg/types" 20 | utilyaml "k8s.io/apimachinery/pkg/util/yaml" 21 | "k8s.io/client-go/discovery" 22 | "k8s.io/client-go/discovery/cached/memory" 23 | "k8s.io/client-go/dynamic" 24 | "k8s.io/client-go/rest" 25 | "k8s.io/client-go/restmapper" 26 | ) 27 | 28 | var ( 29 | issuerName = "e2e" 30 | certificateName = "e2e" 31 | certificateNamespace = "default" 32 | ) 33 | 34 | func ApplyCRD(ctx context.Context, cfg *rest.Config) error { 35 | p, err := os.Getwd() 36 | if err != nil { 37 | return err 38 | } 39 | path := filepath.Join(p, "../config/crd/operator.h3poteto.dev_endpointgroupbindings.yaml") 40 | 41 | buf, err := os.ReadFile(path) 42 | if err != nil { 43 | return err 44 | } 45 | return apply(ctx, cfg, buf) 46 | } 47 | 48 | func ApplyWebhook(ctx context.Context, cfg *rest.Config, serviceNS, serviceName, serviceEndpoint string) error { 49 | webhookconfiguration, err := templates.WebhookConfiguration(certificateNamespace, certificateName, serviceNS, serviceName, serviceEndpoint) 50 | if err != nil { 51 | return err 52 | } 53 | return apply(ctx, cfg, webhookconfiguration.Bytes()) 54 | } 55 | 56 | func ApplyIssuer(ctx context.Context, cfg *rest.Config) error { 57 | issuer, err := templates.Issuer(issuerName, certificateNamespace) 58 | if err != nil { 59 | return err 60 | } 61 | return apply(ctx, cfg, issuer.Bytes()) 62 | } 63 | 64 | func ApplyCertificate(ctx context.Context, cfg *rest.Config, serivceNS, service, secretNS, secret string) error { 65 | certificate, err := templates.Certificate(certificateName, certificateNamespace, issuerName, service, secret) 66 | if err != nil { 67 | return err 68 | } 69 | return apply(ctx, cfg, certificate.Bytes()) 70 | } 71 | 72 | func apply(ctx context.Context, cfg *rest.Config, data []byte) error { 73 | return restAction(ctx, cfg, data, func(ctx context.Context, dr dynamic.ResourceInterface, obj *unstructured.Unstructured, data []byte) error { 74 | _, err := dr.Patch(ctx, obj.GetName(), types.ApplyPatchType, data, v1.PatchOptions{ 75 | FieldManager: "e2e", 76 | }) 77 | return err 78 | }) 79 | } 80 | 81 | type actionFunc func(ctx context.Context, dr dynamic.ResourceInterface, obj *unstructured.Unstructured, data []byte) error 82 | 83 | func restAction(ctx context.Context, cfg *rest.Config, data []byte, action actionFunc) error { 84 | dc, err := discovery.NewDiscoveryClientForConfig(cfg) 85 | if err != nil { 86 | return err 87 | } 88 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc)) 89 | 90 | dyn, err := dynamic.NewForConfig(cfg) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | multidocReader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data))) 96 | 97 | for { 98 | buf, err := multidocReader.Read() 99 | if err != nil { 100 | if err == io.EOF { 101 | return nil 102 | } 103 | return err 104 | } 105 | 106 | var typeMeta runtime.TypeMeta 107 | if err := yaml.Unmarshal(buf, &typeMeta); err != nil { 108 | continue 109 | } 110 | if typeMeta.Kind == "" { 111 | continue 112 | } 113 | 114 | obj := &unstructured.Unstructured{} 115 | _, gvk, err := serializeryaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).Decode(buf, nil, obj) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | var dr dynamic.ResourceInterface 126 | if mapping.Scope.Name() == meta.RESTScopeNameNamespace { 127 | dr = dyn.Resource(mapping.Resource).Namespace(obj.GetNamespace()) 128 | } else { 129 | dr = dyn.Resource(mapping.Resource) 130 | } 131 | 132 | data, err := json.Marshal(obj) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | if err := action(ctx, dr, obj, data); err != nil { 138 | return err 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package versioned 20 | 21 | import ( 22 | fmt "fmt" 23 | http "net/http" 24 | 25 | operatorv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned/typed/endpointgroupbinding/v1alpha1" 26 | discovery "k8s.io/client-go/discovery" 27 | rest "k8s.io/client-go/rest" 28 | flowcontrol "k8s.io/client-go/util/flowcontrol" 29 | ) 30 | 31 | type Interface interface { 32 | Discovery() discovery.DiscoveryInterface 33 | OperatorV1alpha1() operatorv1alpha1.OperatorV1alpha1Interface 34 | } 35 | 36 | // Clientset contains the clients for groups. 37 | type Clientset struct { 38 | *discovery.DiscoveryClient 39 | operatorV1alpha1 *operatorv1alpha1.OperatorV1alpha1Client 40 | } 41 | 42 | // OperatorV1alpha1 retrieves the OperatorV1alpha1Client 43 | func (c *Clientset) OperatorV1alpha1() operatorv1alpha1.OperatorV1alpha1Interface { 44 | return c.operatorV1alpha1 45 | } 46 | 47 | // Discovery retrieves the DiscoveryClient 48 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 49 | if c == nil { 50 | return nil 51 | } 52 | return c.DiscoveryClient 53 | } 54 | 55 | // NewForConfig creates a new Clientset for the given config. 56 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 57 | // NewForConfig will generate a rate-limiter in configShallowCopy. 58 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 59 | // where httpClient was generated with rest.HTTPClientFor(c). 60 | func NewForConfig(c *rest.Config) (*Clientset, error) { 61 | configShallowCopy := *c 62 | 63 | if configShallowCopy.UserAgent == "" { 64 | configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() 65 | } 66 | 67 | // share the transport between all clients 68 | httpClient, err := rest.HTTPClientFor(&configShallowCopy) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return NewForConfigAndClient(&configShallowCopy, httpClient) 74 | } 75 | 76 | // NewForConfigAndClient creates a new Clientset for the given config and http client. 77 | // Note the http client provided takes precedence over the configured transport values. 78 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 79 | // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. 80 | func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { 81 | configShallowCopy := *c 82 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 83 | if configShallowCopy.Burst <= 0 { 84 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 85 | } 86 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 87 | } 88 | 89 | var cs Clientset 90 | var err error 91 | cs.operatorV1alpha1, err = operatorv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return &cs, nil 101 | } 102 | 103 | // NewForConfigOrDie creates a new Clientset for the given config and 104 | // panics if there is an error in the config. 105 | func NewForConfigOrDie(c *rest.Config) *Clientset { 106 | cs, err := NewForConfig(c) 107 | if err != nil { 108 | panic(err) 109 | } 110 | return cs 111 | } 112 | 113 | // New creates a new Clientset for the given RESTClient. 114 | func New(c rest.Interface) *Clientset { 115 | var cs Clientset 116 | cs.operatorV1alpha1 = operatorv1alpha1.New(c) 117 | 118 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 119 | return &cs 120 | } 121 | -------------------------------------------------------------------------------- /e2e/pkg/fixtures/webhook.go: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/api/resource" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | intstr "k8s.io/apimachinery/pkg/util/intstr" 9 | utilpointer "k8s.io/utils/pointer" 10 | ) 11 | 12 | func WebhookDeployment(name, ns, image, secretName string) *appsv1.Deployment { 13 | return &appsv1.Deployment{ 14 | ObjectMeta: metav1.ObjectMeta{ 15 | Name: name, 16 | Namespace: ns, 17 | Labels: map[string]string{ 18 | "app": "webhook", 19 | }, 20 | }, 21 | Spec: appsv1.DeploymentSpec{ 22 | Replicas: utilpointer.Int32(1), 23 | Selector: &metav1.LabelSelector{ 24 | MatchLabels: map[string]string{ 25 | "app": "webhook", 26 | }, 27 | }, 28 | Template: corev1.PodTemplateSpec{ 29 | ObjectMeta: metav1.ObjectMeta{ 30 | Labels: map[string]string{ 31 | "app": "webhook", 32 | }, 33 | }, 34 | Spec: corev1.PodSpec{ 35 | Volumes: []corev1.Volume{ 36 | { 37 | Name: "webhook-certs", 38 | VolumeSource: corev1.VolumeSource{ 39 | Secret: &corev1.SecretVolumeSource{ 40 | SecretName: secretName, 41 | }, 42 | }, 43 | }, 44 | }, 45 | Containers: []corev1.Container{ 46 | { 47 | Name: "webhook", 48 | Image: image, 49 | Args: []string{ 50 | "/aws-global-accelerator-controller", 51 | "webhook", 52 | "--tls-cert-file=/etc/webhook/certs/" + "tls.crt", // CertManager generates a secret with this name 53 | "--tls-private-key-file=/etc/webhook/certs/" + "tls.key", 54 | }, 55 | Resources: corev1.ResourceRequirements{ 56 | Limits: map[corev1.ResourceName]resource.Quantity{ 57 | corev1.ResourceMemory: { 58 | Format: resource.Format("500Mi"), 59 | }, 60 | corev1.ResourceCPU: { 61 | Format: resource.Format("1000m"), 62 | }, 63 | }, 64 | Requests: map[corev1.ResourceName]resource.Quantity{ 65 | corev1.ResourceMemory: { 66 | Format: resource.Format("200Mi"), 67 | }, 68 | corev1.ResourceCPU: { 69 | Format: resource.Format("100m"), 70 | }, 71 | }, 72 | }, 73 | VolumeMounts: []corev1.VolumeMount{ 74 | { 75 | Name: "webhook-certs", 76 | ReadOnly: true, 77 | MountPath: "/etc/webhook/certs", 78 | }, 79 | }, 80 | Ports: []corev1.ContainerPort{ 81 | { 82 | Name: "https", 83 | ContainerPort: 8443, 84 | Protocol: corev1.ProtocolTCP, 85 | }, 86 | }, 87 | LivenessProbe: &corev1.Probe{ 88 | ProbeHandler: corev1.ProbeHandler{ 89 | TCPSocket: &corev1.TCPSocketAction{ 90 | Port: intstr.FromInt(8443), 91 | }, 92 | }, 93 | InitialDelaySeconds: 30, 94 | TimeoutSeconds: 60, 95 | PeriodSeconds: 20, 96 | SuccessThreshold: 1, 97 | FailureThreshold: 4, 98 | }, 99 | ReadinessProbe: &corev1.Probe{ 100 | ProbeHandler: corev1.ProbeHandler{ 101 | TCPSocket: &corev1.TCPSocketAction{ 102 | Port: intstr.FromInt(8443), 103 | }, 104 | }, 105 | InitialDelaySeconds: 30, 106 | TimeoutSeconds: 60, 107 | PeriodSeconds: 10, 108 | SuccessThreshold: 2, 109 | FailureThreshold: 2, 110 | }, 111 | ImagePullPolicy: corev1.PullAlways, 112 | }, 113 | }, 114 | }, 115 | }, 116 | }, 117 | } 118 | 119 | } 120 | 121 | func WebhookService(name, ns string) *corev1.Service { 122 | return &corev1.Service{ 123 | ObjectMeta: metav1.ObjectMeta{ 124 | Name: name, 125 | Namespace: ns, 126 | }, 127 | Spec: corev1.ServiceSpec{ 128 | Ports: []corev1.ServicePort{ 129 | { 130 | Name: "https", 131 | Port: 443, 132 | TargetPort: intstr.FromInt(8443), 133 | Protocol: corev1.ProtocolTCP, 134 | }, 135 | }, 136 | Selector: map[string]string{ 137 | "app": "webhook", 138 | }, 139 | Type: corev1.ServiceTypeClusterIP, 140 | }, 141 | Status: corev1.ServiceStatus{ 142 | LoadBalancer: corev1.LoadBalancerStatus{ 143 | Ingress: []corev1.LoadBalancerIngress{}, 144 | }, 145 | Conditions: []metav1.Condition{}, 146 | }, 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /pkg/controller/globalaccelerator/service.go: -------------------------------------------------------------------------------- 1 | package globalaccelerator 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis" 7 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/cloudprovider" 8 | cloudaws "github.com/h3poteto/aws-global-accelerator-controller/pkg/cloudprovider/aws" 9 | pkgerrors "github.com/h3poteto/aws-global-accelerator-controller/pkg/errors" 10 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/reconcile" 11 | 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/client-go/tools/cache" 15 | "k8s.io/klog/v2" 16 | ) 17 | 18 | func wasLoadBalancerService(svc *corev1.Service) bool { 19 | if svc.Spec.Type == corev1.ServiceTypeLoadBalancer { 20 | if _, ok := svc.Annotations[apis.AWSLoadBalancerTypeAnnotation]; ok || svc.Spec.LoadBalancerClass != nil { 21 | return true 22 | } 23 | } 24 | 25 | return false 26 | } 27 | 28 | func (c *GlobalAcceleratorController) processServiceDelete(ctx context.Context, key string) (reconcile.Result, error) { 29 | klog.Infof("%v has been deleted", key) 30 | ns, name, err := cache.SplitMetaNamespaceKey(key) 31 | if err != nil { 32 | return reconcile.Result{}, pkgerrors.NewNoRetryErrorf("invalid resource key: %s", key) 33 | } 34 | 35 | cloud, err := cloudaws.NewAWS("us-west-2") 36 | if err != nil { 37 | klog.Error(err) 38 | return reconcile.Result{}, err 39 | } 40 | accelerators, err := cloud.ListGlobalAcceleratorByResource(ctx, c.clusterName, "service", ns, name) 41 | if err != nil { 42 | klog.Error(err) 43 | return reconcile.Result{}, err 44 | } 45 | for _, accelerator := range accelerators { 46 | if err := cloud.CleanupGlobalAccelerator(ctx, *accelerator.AcceleratorArn); err != nil { 47 | klog.Error(err) 48 | return reconcile.Result{}, err 49 | } 50 | } 51 | return reconcile.Result{}, err 52 | } 53 | 54 | func (c *GlobalAcceleratorController) processServiceCreateOrUpdate(ctx context.Context, obj runtime.Object) (reconcile.Result, error) { 55 | svc, ok := obj.(*corev1.Service) 56 | if !ok { 57 | return reconcile.Result{}, pkgerrors.NewNoRetryErrorf("object is not Service, it is %T", obj) 58 | } 59 | if len(svc.Status.LoadBalancer.Ingress) < 1 { 60 | klog.Warningf("%s/%s does not have ingress LoadBalancer, so skip it", svc.Namespace, svc.Name) 61 | return reconcile.Result{}, nil 62 | } 63 | 64 | if _, ok := svc.Annotations[apis.AWSGlobalAcceleratorManagedAnnotation]; !ok { 65 | cloud, err := cloudaws.NewAWS("us-west-2") 66 | if err != nil { 67 | klog.Error(err) 68 | return reconcile.Result{}, err 69 | } 70 | accelerators, err := cloud.ListGlobalAcceleratorByResource(ctx, c.clusterName, "service", svc.Namespace, svc.Name) 71 | if err != nil { 72 | klog.Error(err) 73 | return reconcile.Result{}, err 74 | } 75 | for _, accelerator := range accelerators { 76 | if err := cloud.CleanupGlobalAccelerator(ctx, *accelerator.AcceleratorArn); err != nil { 77 | klog.Error(err) 78 | return reconcile.Result{}, err 79 | } 80 | } 81 | klog.Infof("Delete Global Accelerator for Service %s/%s", svc.Namespace, svc.Name) 82 | c.recorder.Event(svc, corev1.EventTypeNormal, "GlobalAcceleratorDeleted", "Global Accelerators are deleted") 83 | return reconcile.Result{}, nil 84 | } 85 | 86 | for i := range svc.Status.LoadBalancer.Ingress { 87 | lbIngress := svc.Status.LoadBalancer.Ingress[i] 88 | provider, err := cloudprovider.DetectCloudProvider(lbIngress.Hostname) 89 | if err != nil { 90 | klog.Error(err) 91 | continue 92 | } 93 | switch provider { 94 | case "aws": 95 | // Get load balancer name and region from the hostname 96 | name, region, err := cloudaws.GetLBNameFromHostname(lbIngress.Hostname) 97 | if err != nil { 98 | klog.Error(err) 99 | return reconcile.Result{}, err 100 | } 101 | cloud, err := cloudaws.NewAWS(region) 102 | if err != nil { 103 | klog.Error(err) 104 | return reconcile.Result{}, err 105 | } 106 | arn, created, retryAfter, err := cloud.EnsureGlobalAcceleratorForService(ctx, svc, &lbIngress, c.clusterName, name, region) 107 | if err != nil { 108 | return reconcile.Result{}, err 109 | } 110 | if retryAfter > 0 { 111 | return reconcile.Result{ 112 | Requeue: true, 113 | RequeueAfter: retryAfter, 114 | }, nil 115 | } 116 | if created { 117 | c.recorder.Eventf(svc, corev1.EventTypeNormal, "GlobalAcceleratorCreated", "Global Acclerator is created: %s", *arn) 118 | } 119 | default: 120 | klog.Warningf("Not implemented for %s", provider) 121 | continue 122 | } 123 | } 124 | 125 | return reconcile.Result{}, nil 126 | } 127 | -------------------------------------------------------------------------------- /pkg/controller/globalaccelerator/ingress.go: -------------------------------------------------------------------------------- 1 | package globalaccelerator 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis" 7 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/cloudprovider" 8 | cloudaws "github.com/h3poteto/aws-global-accelerator-controller/pkg/cloudprovider/aws" 9 | pkgerrors "github.com/h3poteto/aws-global-accelerator-controller/pkg/errors" 10 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/reconcile" 11 | 12 | corev1 "k8s.io/api/core/v1" 13 | networkingv1 "k8s.io/api/networking/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/client-go/tools/cache" 16 | "k8s.io/klog/v2" 17 | ) 18 | 19 | func wasALBIngress(ingress *networkingv1.Ingress) bool { 20 | if ingress.Spec.IngressClassName != nil && *ingress.Spec.IngressClassName == "alb" { 21 | return true 22 | } 23 | if _, ok := ingress.Annotations[apis.IngressClassAnnotation]; ok { 24 | return true 25 | } 26 | return false 27 | } 28 | 29 | func (c *GlobalAcceleratorController) processIngressDelete(ctx context.Context, key string) (reconcile.Result, error) { 30 | klog.Infof("%v has been deleted", key) 31 | ns, name, err := cache.SplitMetaNamespaceKey(key) 32 | if err != nil { 33 | return reconcile.Result{}, pkgerrors.NewNoRetryErrorf("invalid resource key: %s", key) 34 | } 35 | 36 | cloud, err := cloudaws.NewAWS("us-west-2") 37 | if err != nil { 38 | klog.Error(err) 39 | return reconcile.Result{}, err 40 | } 41 | 42 | accelerators, err := cloud.ListGlobalAcceleratorByResource(ctx, c.clusterName, "ingress", ns, name) 43 | if err != nil { 44 | klog.Error(err) 45 | return reconcile.Result{}, err 46 | } 47 | for _, accelerator := range accelerators { 48 | if err := cloud.CleanupGlobalAccelerator(ctx, *accelerator.AcceleratorArn); err != nil { 49 | klog.Error(err) 50 | return reconcile.Result{}, err 51 | } 52 | } 53 | return reconcile.Result{}, err 54 | } 55 | 56 | func (c *GlobalAcceleratorController) processIngressCreateOrUpdate(ctx context.Context, obj runtime.Object) (reconcile.Result, error) { 57 | ingress, ok := obj.(*networkingv1.Ingress) 58 | if !ok { 59 | return reconcile.Result{}, pkgerrors.NewNoRetryErrorf("object is not Ingress, it is %T", obj) 60 | } 61 | if len(ingress.Status.LoadBalancer.Ingress) < 1 { 62 | klog.Warningf("%s/%s does not have ingress LoadBalancer, so skip it", ingress.Namespace, ingress.Name) 63 | return reconcile.Result{}, nil 64 | } 65 | 66 | if _, ok := ingress.Annotations[apis.AWSGlobalAcceleratorManagedAnnotation]; !ok { 67 | cloud, err := cloudaws.NewAWS("us-west-2") 68 | if err != nil { 69 | klog.Error(err) 70 | return reconcile.Result{}, err 71 | } 72 | accelerators, err := cloud.ListGlobalAcceleratorByResource(ctx, c.clusterName, "ingress", ingress.Namespace, ingress.Name) 73 | if err != nil { 74 | klog.Error(err) 75 | return reconcile.Result{}, err 76 | } 77 | for _, a := range accelerators { 78 | klog.Infof("Ingress %s/%s does not have the annotation, but Global Accelerator exists, so deleting this", ingress.Namespace, ingress.Name) 79 | err = cloud.CleanupGlobalAccelerator(ctx, *a.AcceleratorArn) 80 | if err != nil { 81 | klog.Error(err) 82 | return reconcile.Result{}, err 83 | } 84 | } 85 | klog.Infof("Delete Global Accelerator for Ingress %s/%s", ingress.Namespace, ingress.Name) 86 | c.recorder.Event(ingress, corev1.EventTypeNormal, "GlobalAcceleratorDeleted", "Global Accelerator are deleted") 87 | return reconcile.Result{}, nil 88 | } 89 | 90 | for i := range ingress.Status.LoadBalancer.Ingress { 91 | lbIngress := ingress.Status.LoadBalancer.Ingress[i] 92 | provider, err := cloudprovider.DetectCloudProvider(lbIngress.Hostname) 93 | if err != nil { 94 | klog.Error(err) 95 | continue 96 | } 97 | switch provider { 98 | case "aws": 99 | // Get load balancer name and region from the hostname 100 | name, region, err := cloudaws.GetLBNameFromHostname(lbIngress.Hostname) 101 | if err != nil { 102 | klog.Error(err) 103 | return reconcile.Result{}, err 104 | } 105 | cloud, err := cloudaws.NewAWS(region) 106 | if err != nil { 107 | klog.Error(err) 108 | return reconcile.Result{}, err 109 | } 110 | arn, created, retryAfter, err := cloud.EnsureGlobalAcceleratorForIngress(ctx, ingress, &lbIngress, c.clusterName, name, region) 111 | if err != nil { 112 | return reconcile.Result{}, err 113 | } 114 | if retryAfter > 0 { 115 | return reconcile.Result{ 116 | Requeue: true, 117 | RequeueAfter: retryAfter, 118 | }, nil 119 | } 120 | if created { 121 | c.recorder.Eventf(ingress, corev1.EventTypeNormal, "GlobalAcceleratorCreated", "Global Acclerator is created: %s", *arn) 122 | } 123 | default: 124 | klog.Warningf("Not implemented for %s", provider) 125 | continue 126 | } 127 | } 128 | 129 | return reconcile.Result{}, nil 130 | } 131 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/endpointgroupbinding/v1alpha1/endpointgroupbinding.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 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 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | context "context" 23 | time "time" 24 | 25 | apisendpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis/endpointgroupbinding/v1alpha1" 26 | versioned "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned" 27 | internalinterfaces "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/informers/externalversions/internalinterfaces" 28 | endpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/listers/endpointgroupbinding/v1alpha1" 29 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | runtime "k8s.io/apimachinery/pkg/runtime" 31 | watch "k8s.io/apimachinery/pkg/watch" 32 | cache "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | // EndpointGroupBindingInformer provides access to a shared informer and lister for 36 | // EndpointGroupBindings. 37 | type EndpointGroupBindingInformer interface { 38 | Informer() cache.SharedIndexInformer 39 | Lister() endpointgroupbindingv1alpha1.EndpointGroupBindingLister 40 | } 41 | 42 | type endpointGroupBindingInformer struct { 43 | factory internalinterfaces.SharedInformerFactory 44 | tweakListOptions internalinterfaces.TweakListOptionsFunc 45 | namespace string 46 | } 47 | 48 | // NewEndpointGroupBindingInformer constructs a new informer for EndpointGroupBinding type. 49 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 50 | // one. This reduces memory footprint and number of connections to the server. 51 | func NewEndpointGroupBindingInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 52 | return NewFilteredEndpointGroupBindingInformer(client, namespace, resyncPeriod, indexers, nil) 53 | } 54 | 55 | // NewFilteredEndpointGroupBindingInformer constructs a new informer for EndpointGroupBinding type. 56 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 57 | // one. This reduces memory footprint and number of connections to the server. 58 | func NewFilteredEndpointGroupBindingInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 59 | return cache.NewSharedIndexInformer( 60 | &cache.ListWatch{ 61 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 62 | if tweakListOptions != nil { 63 | tweakListOptions(&options) 64 | } 65 | return client.OperatorV1alpha1().EndpointGroupBindings(namespace).List(context.Background(), options) 66 | }, 67 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 68 | if tweakListOptions != nil { 69 | tweakListOptions(&options) 70 | } 71 | return client.OperatorV1alpha1().EndpointGroupBindings(namespace).Watch(context.Background(), options) 72 | }, 73 | ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { 74 | if tweakListOptions != nil { 75 | tweakListOptions(&options) 76 | } 77 | return client.OperatorV1alpha1().EndpointGroupBindings(namespace).List(ctx, options) 78 | }, 79 | WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { 80 | if tweakListOptions != nil { 81 | tweakListOptions(&options) 82 | } 83 | return client.OperatorV1alpha1().EndpointGroupBindings(namespace).Watch(ctx, options) 84 | }, 85 | }, 86 | &apisendpointgroupbindingv1alpha1.EndpointGroupBinding{}, 87 | resyncPeriod, 88 | indexers, 89 | ) 90 | } 91 | 92 | func (f *endpointGroupBindingInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 93 | return NewFilteredEndpointGroupBindingInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 94 | } 95 | 96 | func (f *endpointGroupBindingInformer) Informer() cache.SharedIndexInformer { 97 | return f.factory.InformerFor(&apisendpointgroupbindingv1alpha1.EndpointGroupBinding{}, f.defaultInformer) 98 | } 99 | 100 | func (f *endpointGroupBindingInformer) Lister() endpointgroupbindingv1alpha1.EndpointGroupBindingLister { 101 | return endpointgroupbindingv1alpha1.NewEndpointGroupBindingLister(f.Informer().GetIndexer()) 102 | } 103 | -------------------------------------------------------------------------------- /pkg/cloudprovider/aws/route53_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | gatypes "github.com/aws/aws-sdk-go-v2/service/globalaccelerator/types" 8 | route53types "github.com/aws/aws-sdk-go-v2/service/route53/types" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestFindARecord(t *testing.T) { 13 | cases := []struct { 14 | title string 15 | records []route53types.ResourceRecordSet 16 | hostname string 17 | expected *route53types.ResourceRecordSet 18 | }{ 19 | { 20 | title: "Does not contain A record", 21 | records: []route53types.ResourceRecordSet{ 22 | route53types.ResourceRecordSet{ 23 | Name: aws.String("foo.example.com."), 24 | Type: route53types.RRTypeCname, 25 | }, 26 | route53types.ResourceRecordSet{ 27 | Name: aws.String("bar.example.com."), 28 | Type: route53types.RRTypeCname, 29 | }, 30 | }, 31 | hostname: "foo.example.com", 32 | expected: nil, 33 | }, 34 | { 35 | title: "Does not contain hostname", 36 | records: []route53types.ResourceRecordSet{ 37 | route53types.ResourceRecordSet{ 38 | Name: aws.String("foo.example.com."), 39 | Type: route53types.RRTypeA, 40 | }, 41 | route53types.ResourceRecordSet{ 42 | Name: aws.String("bar.example.com."), 43 | Type: route53types.RRTypeA, 44 | }, 45 | }, 46 | hostname: "baz.example.com", 47 | expected: nil, 48 | }, 49 | { 50 | title: "Contains hostname", 51 | records: []route53types.ResourceRecordSet{ 52 | route53types.ResourceRecordSet{ 53 | Name: aws.String("foo.example.com."), 54 | Type: route53types.RRTypeA, 55 | }, 56 | route53types.ResourceRecordSet{ 57 | Name: aws.String("bar.example.com."), 58 | Type: route53types.RRTypeA, 59 | }, 60 | }, 61 | hostname: "bar.example.com", 62 | expected: &route53types.ResourceRecordSet{ 63 | Name: aws.String("bar.example.com."), 64 | Type: route53types.RRTypeA, 65 | }, 66 | }, 67 | { 68 | title: "Contains wildcard record", 69 | records: []route53types.ResourceRecordSet{ 70 | route53types.ResourceRecordSet{ 71 | Name: aws.String("\\052.example.com."), 72 | Type: route53types.RRTypeA, 73 | }, 74 | route53types.ResourceRecordSet{ 75 | Name: aws.String("bar.example.com."), 76 | Type: route53types.RRTypeA, 77 | }, 78 | }, 79 | hostname: "*.example.com", 80 | expected: &route53types.ResourceRecordSet{ 81 | Name: aws.String("\\052.example.com."), 82 | Type: route53types.RRTypeA, 83 | }, 84 | }, 85 | } 86 | for _, c := range cases { 87 | t.Run(c.title, func(tt *testing.T) { 88 | result := findARecord(c.records, c.hostname) 89 | assert.Equal(tt, c.expected, result) 90 | }) 91 | } 92 | } 93 | 94 | func TestNeedRecordsUpdate(t *testing.T) { 95 | cases := []struct { 96 | title string 97 | record *route53types.ResourceRecordSet 98 | accelerator *gatypes.Accelerator 99 | expected bool 100 | }{ 101 | { 102 | title: "Alias is nil", 103 | record: &route53types.ResourceRecordSet{ 104 | Name: aws.String("foo.example.com"), 105 | }, 106 | accelerator: &gatypes.Accelerator{}, 107 | expected: true, 108 | }, 109 | { 110 | title: "Alias DNS name is not matched", 111 | record: &route53types.ResourceRecordSet{ 112 | Name: aws.String("foo.example.com"), 113 | AliasTarget: &route53types.AliasTarget{ 114 | DNSName: aws.String("foo.example.com."), 115 | }, 116 | }, 117 | accelerator: &gatypes.Accelerator{ 118 | DnsName: aws.String("bar.example.com"), 119 | }, 120 | expected: true, 121 | }, 122 | { 123 | title: "Alias DNS name is matched", 124 | record: &route53types.ResourceRecordSet{ 125 | Name: aws.String("foo.example.com"), 126 | AliasTarget: &route53types.AliasTarget{ 127 | DNSName: aws.String("foo.example.com."), 128 | }, 129 | }, 130 | accelerator: &gatypes.Accelerator{ 131 | DnsName: aws.String("foo.example.com"), 132 | }, 133 | expected: false, 134 | }, 135 | } 136 | for _, c := range cases { 137 | t.Run(c.title, func(tt *testing.T) { 138 | result := needRecordsUpdate(c.record, c.accelerator) 139 | assert.Equal(tt, c.expected, result) 140 | }) 141 | } 142 | } 143 | 144 | func TestParentDomain(t *testing.T) { 145 | cases := []struct { 146 | title string 147 | hostname string 148 | expected string 149 | }{ 150 | { 151 | title: "Hostname is subdomain", 152 | hostname: "h3poteto-test.example.com", 153 | expected: "example.com", 154 | }, 155 | { 156 | title: "Hostname is sub-subdomain", 157 | hostname: "h3poteto-test.foo.example.com", 158 | expected: "foo.example.com", 159 | }, 160 | { 161 | title: "Hostname is domain", 162 | hostname: "example.com", 163 | expected: "com", 164 | }, 165 | { 166 | title: "Hostname is top-level domain", 167 | hostname: "com", 168 | expected: "", 169 | }, 170 | { 171 | title: "Hostname is a dot", 172 | hostname: ".", 173 | expected: "", 174 | }, 175 | } 176 | 177 | for _, c := range cases { 178 | t.Run(c.title, func(tt *testing.T) { 179 | result := parentDomain(c.hostname) 180 | assert.Equal(tt, c.expected, result) 181 | }) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /pkg/apis/endpointgroupbinding/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by deepcopy-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *EndpointGroupBinding) DeepCopyInto(out *EndpointGroupBinding) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | in.Status.DeepCopyInto(&out.Status) 35 | return 36 | } 37 | 38 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointGroupBinding. 39 | func (in *EndpointGroupBinding) DeepCopy() *EndpointGroupBinding { 40 | if in == nil { 41 | return nil 42 | } 43 | out := new(EndpointGroupBinding) 44 | in.DeepCopyInto(out) 45 | return out 46 | } 47 | 48 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 49 | func (in *EndpointGroupBinding) DeepCopyObject() runtime.Object { 50 | if c := in.DeepCopy(); c != nil { 51 | return c 52 | } 53 | return nil 54 | } 55 | 56 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 57 | func (in *EndpointGroupBindingList) DeepCopyInto(out *EndpointGroupBindingList) { 58 | *out = *in 59 | out.TypeMeta = in.TypeMeta 60 | in.ListMeta.DeepCopyInto(&out.ListMeta) 61 | if in.Items != nil { 62 | in, out := &in.Items, &out.Items 63 | *out = make([]EndpointGroupBinding, len(*in)) 64 | for i := range *in { 65 | (*in)[i].DeepCopyInto(&(*out)[i]) 66 | } 67 | } 68 | return 69 | } 70 | 71 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointGroupBindingList. 72 | func (in *EndpointGroupBindingList) DeepCopy() *EndpointGroupBindingList { 73 | if in == nil { 74 | return nil 75 | } 76 | out := new(EndpointGroupBindingList) 77 | in.DeepCopyInto(out) 78 | return out 79 | } 80 | 81 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 82 | func (in *EndpointGroupBindingList) DeepCopyObject() runtime.Object { 83 | if c := in.DeepCopy(); c != nil { 84 | return c 85 | } 86 | return nil 87 | } 88 | 89 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 90 | func (in *EndpointGroupBindingSpec) DeepCopyInto(out *EndpointGroupBindingSpec) { 91 | *out = *in 92 | if in.Weight != nil { 93 | in, out := &in.Weight, &out.Weight 94 | *out = new(int32) 95 | **out = **in 96 | } 97 | if in.ServiceRef != nil { 98 | in, out := &in.ServiceRef, &out.ServiceRef 99 | *out = new(ServiceReference) 100 | **out = **in 101 | } 102 | if in.IngressRef != nil { 103 | in, out := &in.IngressRef, &out.IngressRef 104 | *out = new(IngressReference) 105 | **out = **in 106 | } 107 | return 108 | } 109 | 110 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointGroupBindingSpec. 111 | func (in *EndpointGroupBindingSpec) DeepCopy() *EndpointGroupBindingSpec { 112 | if in == nil { 113 | return nil 114 | } 115 | out := new(EndpointGroupBindingSpec) 116 | in.DeepCopyInto(out) 117 | return out 118 | } 119 | 120 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 121 | func (in *EndpointGroupBindingStatus) DeepCopyInto(out *EndpointGroupBindingStatus) { 122 | *out = *in 123 | if in.EndpointIds != nil { 124 | in, out := &in.EndpointIds, &out.EndpointIds 125 | *out = make([]string, len(*in)) 126 | copy(*out, *in) 127 | } 128 | return 129 | } 130 | 131 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointGroupBindingStatus. 132 | func (in *EndpointGroupBindingStatus) DeepCopy() *EndpointGroupBindingStatus { 133 | if in == nil { 134 | return nil 135 | } 136 | out := new(EndpointGroupBindingStatus) 137 | in.DeepCopyInto(out) 138 | return out 139 | } 140 | 141 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 142 | func (in *IngressReference) DeepCopyInto(out *IngressReference) { 143 | *out = *in 144 | return 145 | } 146 | 147 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressReference. 148 | func (in *IngressReference) DeepCopy() *IngressReference { 149 | if in == nil { 150 | return nil 151 | } 152 | out := new(IngressReference) 153 | in.DeepCopyInto(out) 154 | return out 155 | } 156 | 157 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 158 | func (in *ServiceReference) DeepCopyInto(out *ServiceReference) { 159 | *out = *in 160 | return 161 | } 162 | 163 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceReference. 164 | func (in *ServiceReference) DeepCopy() *ServiceReference { 165 | if in == nil { 166 | return nil 167 | } 168 | out := new(ServiceReference) 169 | in.DeepCopyInto(out) 170 | return out 171 | } 172 | -------------------------------------------------------------------------------- /local_e2e/cluster.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kops.k8s.io/v1alpha2 2 | kind: Cluster 3 | metadata: 4 | creationTimestamp: null 5 | name: e2e-global-accelerator.k8s.h3poteto.dev 6 | spec: 7 | api: 8 | dns: {} 9 | authorization: 10 | rbac: {} 11 | channel: stable 12 | cloudProvider: aws 13 | configBase: s3://e2e-global-accelerator/e2e-global-accelerator.k8s.h3poteto.dev 14 | dnsZone: Z21DTFU4BHSZ51 15 | containerRuntime: containerd 16 | etcdClusters: 17 | - cpuRequest: 200m 18 | etcdMembers: 19 | - encryptedVolume: true 20 | instanceGroup: control-plane-ap-northeast-1a 21 | name: a 22 | manager: 23 | backupRetentionDays: 90 24 | memoryRequest: 100Mi 25 | name: main 26 | - cpuRequest: 100m 27 | etcdMembers: 28 | - encryptedVolume: true 29 | instanceGroup: control-plane-ap-northeast-1a 30 | name: a 31 | manager: 32 | backupRetentionDays: 90 33 | memoryRequest: 100Mi 34 | name: events 35 | iam: 36 | allowContainerRegistry: true 37 | legacy: false 38 | serviceAccountExternalPermissions: 39 | - name: aws-global-accelerator-controller 40 | namespace: default 41 | aws: 42 | inlinePolicy: |- 43 | [ 44 | { 45 | "Effect": "Allow", 46 | "Action": [ 47 | "elasticloadbalancing:DescribeLoadBalancers", 48 | "globalaccelerator:DescribeAccelerator", 49 | "globalaccelerator:ListAccelerators", 50 | "globalaccelerator:ListTagsForResource", 51 | "globalaccelerator:TagResource", 52 | "globalaccelerator:CreateAccelerator", 53 | "globalaccelerator:UpdateAccelerator", 54 | "globalaccelerator:DeleteAccelerator", 55 | "globalaccelerator:ListListeners", 56 | "globalaccelerator:CreateListener", 57 | "globalaccelerator:UpdateListener", 58 | "globalaccelerator:DeleteListener", 59 | "globalaccelerator:ListEndpointGroups", 60 | "globalaccelerator:CreateEndpointGroup", 61 | "globalaccelerator:UpdateEndpointGroup", 62 | "globalaccelerator:DeleteEndpointGroup", 63 | "globalaccelerator:AddEndpoints", 64 | "globalaccelerator:RemoveEndpoints", 65 | "route53:ChangeResourceRecordSets", 66 | "route53:ListHostedZones", 67 | "route53:ListHostedzonesByName", 68 | "route53:ListResourceRecordSets" 69 | ], 70 | "Resource": "*" 71 | } 72 | ] 73 | 74 | kubelet: 75 | anonymousAuth: false 76 | authenticationTokenWebhook: true 77 | authorizationMode: Webhook 78 | maxPods: 50 79 | kubeAPIServer: 80 | defaultNotReadyTolerationSeconds: 600 81 | defaultUnreachableTolerationSeconds: 600 82 | kubernetesApiAccess: 83 | - 0.0.0.0/0 84 | - ::/0 85 | kubernetesVersion: 1.28.8 86 | masterPublicName: api.e2e-global-accelerator.k8s.h3poteto.dev 87 | networkCIDR: 172.20.0.0/16 88 | networking: 89 | amazonvpc: {} 90 | nonMasqueradeCIDR: 100.64.0.0/10 91 | serviceAccountIssuerDiscovery: 92 | discoveryStore: s3://e2e-aws-global-accelerator-discovery 93 | enableAWSOIDCProvider: true 94 | awsLoadBalancerController: 95 | enabled: true 96 | podIdentityWebhook: 97 | enabled: true 98 | certManager: 99 | enabled: true 100 | managed: true 101 | sshAccess: 102 | - 0.0.0.0/0 103 | - ::/0 104 | subnets: 105 | - cidr: 172.20.32.0/19 106 | name: ap-northeast-1a 107 | type: Public 108 | zone: ap-northeast-1a 109 | - cidr: 172.20.64.0/19 110 | name: ap-northeast-1c 111 | type: Public 112 | zone: ap-northeast-1c 113 | - cidr: 172.20.96.0/19 114 | name: ap-northeast-1d 115 | type: Public 116 | zone: ap-northeast-1d 117 | topology: 118 | dns: 119 | type: Public 120 | masters: public 121 | nodes: public 122 | 123 | --- 124 | 125 | apiVersion: kops.k8s.io/v1alpha2 126 | kind: InstanceGroup 127 | metadata: 128 | creationTimestamp: null 129 | labels: 130 | kops.k8s.io/cluster: e2e-global-accelerator.k8s.h3poteto.dev 131 | name: control-plane-ap-northeast-1a 132 | spec: 133 | image: 099720109477/ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20230112 134 | instanceMetadata: 135 | httpPutResponseHopLimit: 3 136 | httpTokens: required 137 | machineType: t3.small 138 | maxSize: 1 139 | minSize: 1 140 | maxPrice: "0.208" 141 | mixedInstancesPolicy: 142 | instances: 143 | - t3.small 144 | - t2.small 145 | onDemandAboveBase: 0 146 | onDemandBase: 0 147 | spotAllocationStrategy: lowest-price 148 | spotInstancePools: 2 149 | nodeLabels: 150 | kops.k8s.io/instancegroup: control-plane-ap-northeast-1a 151 | role: Master 152 | subnets: 153 | - ap-northeast-1a 154 | 155 | --- 156 | 157 | apiVersion: kops.k8s.io/v1alpha2 158 | kind: InstanceGroup 159 | metadata: 160 | creationTimestamp: null 161 | labels: 162 | kops.k8s.io/cluster: e2e-global-accelerator.k8s.h3poteto.dev 163 | name: nodes-ap-northeast-1a 164 | spec: 165 | image: 099720109477/ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20230112 166 | instanceMetadata: 167 | httpPutResponseHopLimit: 3 168 | httpTokens: required 169 | machineType: t3.small 170 | maxSize: 1 171 | minSize: 1 172 | maxPrice: "0.416" 173 | mixedInstancesPolicy: 174 | instances: 175 | - t3.small 176 | - t2.small 177 | onDemandAboveBase: 0 178 | onDemandBase: 0 179 | spotAllocationStrategy: lowest-price 180 | spotInstancePools: 2 181 | nodeLabels: 182 | kops.k8s.io/instancegroup: nodes-ap-northeast-1a 183 | role: Node 184 | subnets: 185 | - ap-northeast-1a 186 | 187 | --- 188 | 189 | apiVersion: kops.k8s.io/v1alpha2 190 | kind: InstanceGroup 191 | metadata: 192 | creationTimestamp: null 193 | labels: 194 | kops.k8s.io/cluster: e2e-global-accelerator.k8s.h3poteto.dev 195 | name: nodes-ap-northeast-1c 196 | spec: 197 | image: 099720109477/ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20230112 198 | instanceMetadata: 199 | httpPutResponseHopLimit: 3 200 | httpTokens: required 201 | machineType: t3.small 202 | maxSize: 1 203 | minSize: 1 204 | maxPrice: "0.416" 205 | mixedInstancesPolicy: 206 | instances: 207 | - t3.small 208 | - t2.small 209 | onDemandAboveBase: 0 210 | onDemandBase: 0 211 | spotAllocationStrategy: lowest-price 212 | spotInstancePools: 2 213 | nodeLabels: 214 | kops.k8s.io/instancegroup: nodes-ap-northeast-1c 215 | role: Node 216 | subnets: 217 | - ap-northeast-1c 218 | -------------------------------------------------------------------------------- /e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/h3poteto/aws-global-accelerator-controller/e2e/pkg/fixtures" 14 | "github.com/h3poteto/aws-global-accelerator-controller/e2e/pkg/util" 15 | ownclientset "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned" 16 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/fixture" 17 | corev1 "k8s.io/api/core/v1" 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | "k8s.io/apimachinery/pkg/util/wait" 20 | "k8s.io/client-go/kubernetes" 21 | "k8s.io/client-go/rest" 22 | "k8s.io/client-go/tools/clientcmd" 23 | "k8s.io/klog/v2" 24 | utilpointer "k8s.io/utils/pointer" 25 | ) 26 | 27 | var ( 28 | cfg *rest.Config 29 | ownClient *ownclientset.Clientset 30 | client *kubernetes.Clientset 31 | resourceNS = "kube-public" 32 | webhookNS = "default" 33 | secretName = "webhook-certs" 34 | serviceName = "webhook-service" 35 | ) 36 | 37 | var _ = BeforeSuite(func() { 38 | configfile := os.Getenv("KUBECONFIG") 39 | if configfile == "" { 40 | configfile = "$HOME/.kube/config" 41 | } 42 | var err error 43 | cfg, err = clientcmd.BuildConfigFromFlags("", os.ExpandEnv(configfile)) 44 | Expect(err).ShouldNot(HaveOccurred()) 45 | 46 | client, err = kubernetes.NewForConfig(cfg) 47 | Expect(err).ShouldNot(HaveOccurred()) 48 | 49 | ownClient, err = ownclientset.NewForConfig(cfg) 50 | Expect(err).ShouldNot(HaveOccurred()) 51 | 52 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) 53 | defer cancel() 54 | 55 | err = waitUntilReady(ctx, client) 56 | Expect(err).ShouldNot(HaveOccurred()) 57 | }) 58 | 59 | var _ = Describe("E2E", func() { 60 | BeforeEach(func() { 61 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) 62 | defer cancel() 63 | 64 | // Apply CRDs 65 | if err := util.ApplyCRD(ctx, cfg); err != nil { 66 | panic(err) 67 | } 68 | // Deployment, service, Certificate, Issuer 69 | if err := applyWebhook(ctx, cfg, client); err != nil { 70 | panic(err) 71 | } 72 | }) 73 | 74 | AfterEach(func() { 75 | // Delete all endpointgroupbinding 76 | ownClient.OperatorV1alpha1().EndpointGroupBindings(resourceNS).DeleteCollection(context.Background(), metav1.DeleteOptions{}, metav1.ListOptions{}) 77 | }) 78 | It("Changing ARN", func() { 79 | resource := fixture.EndpointGroupBinding(false, "example", utilpointer.Int32(100), "arn:aws:globalaccelerator::123456789012:accelerator/1234abcd-abcd-1234-abcd-1234abcd1234") 80 | _, err := ownClient.OperatorV1alpha1().EndpointGroupBindings(resourceNS).Create(context.Background(), &resource, metav1.CreateOptions{}) 81 | Expect(err).ShouldNot(HaveOccurred()) 82 | current, err := ownClient.OperatorV1alpha1().EndpointGroupBindings(resourceNS).Get(context.Background(), resource.Name, metav1.GetOptions{}) 83 | Expect(err).ShouldNot(HaveOccurred()) 84 | current.Spec.EndpointGroupArn = "arn:aws:globalaccelerator::123456789012:accelerator/5678efgh-efgh-5678-efgh-5678efgh5678" 85 | _, err = ownClient.OperatorV1alpha1().EndpointGroupBindings(current.Namespace).Update(context.Background(), current, metav1.UpdateOptions{}) 86 | Expect(err).Should(HaveOccurred()) 87 | Expect(strings.Contains(err.Error(), "Spec.EndpointGroupArn is immutable")).Should(BeTrue()) 88 | }) 89 | It("Changing weight", func() { 90 | resource := fixture.EndpointGroupBinding(false, "example", utilpointer.Int32(100), "arn:aws:globalaccelerator::123456789012:accelerator/1234abcd-abcd-1234-abcd-1234abcd1234") 91 | _, err := ownClient.OperatorV1alpha1().EndpointGroupBindings(resourceNS).Create(context.Background(), &resource, metav1.CreateOptions{}) 92 | Expect(err).ShouldNot(HaveOccurred()) 93 | current, err := ownClient.OperatorV1alpha1().EndpointGroupBindings(resourceNS).Get(context.Background(), resource.Name, metav1.GetOptions{}) 94 | Expect(err).ShouldNot(HaveOccurred()) 95 | current.Spec.Weight = utilpointer.Int32(200) 96 | _, err = ownClient.OperatorV1alpha1().EndpointGroupBindings(current.Namespace).Update(context.Background(), current, metav1.UpdateOptions{}) 97 | Expect(err).ShouldNot(HaveOccurred()) 98 | }) 99 | 100 | }) 101 | 102 | func waitUntilReady(ctx context.Context, client *kubernetes.Clientset) error { 103 | klog.Info("Waiting until kubernetes cluster is ready") 104 | err := wait.Poll(10*time.Second, 10*time.Minute, func() (bool, error) { 105 | nodeList, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) 106 | if err != nil { 107 | return false, fmt.Errorf("failed to list nodes: %v", err) 108 | } 109 | if len(nodeList.Items) == 0 { 110 | klog.Warningf("node does not exist yet") 111 | return false, nil 112 | } 113 | for i := range nodeList.Items { 114 | n := &nodeList.Items[i] 115 | if !nodeIsReady(n) { 116 | klog.Warningf("node %s is not ready yet", n.Name) 117 | return false, nil 118 | } 119 | } 120 | klog.Info("all nodes are ready") 121 | return true, nil 122 | }) 123 | return err 124 | } 125 | 126 | func nodeIsReady(node *corev1.Node) bool { 127 | for i := range node.Status.Conditions { 128 | con := &node.Status.Conditions[i] 129 | if con.Type == corev1.NodeReady && con.Status == corev1.ConditionTrue { 130 | return true 131 | } 132 | } 133 | return false 134 | } 135 | 136 | func applyWebhook(ctx context.Context, cfg *rest.Config, client *kubernetes.Clientset) error { 137 | // Apply Issuer 138 | err := util.ApplyIssuer(ctx, cfg) 139 | if err != nil { 140 | return err 141 | } 142 | // Apply Certificate 143 | err = util.ApplyCertificate(ctx, cfg, webhookNS, serviceName, webhookNS, secretName) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | // Apply webhook configuration manifests 149 | if err := util.ApplyWebhook(ctx, cfg, webhookNS, serviceName, "/validate-endpointgroupbinding"); err != nil { 150 | return err 151 | } 152 | 153 | // Apply Deployment 154 | image := os.Getenv("WEBHOOK_IMAGE") 155 | deploy := fixtures.WebhookDeployment("webhook", webhookNS, image, secretName) 156 | if _, err := client.AppsV1().Deployments(webhookNS).Get(ctx, deploy.Name, metav1.GetOptions{}); err != nil { 157 | _, err = client.AppsV1().Deployments(webhookNS).Create(ctx, deploy, metav1.CreateOptions{}) 158 | if err != nil { 159 | return err 160 | } 161 | } 162 | // Apply Service 163 | service := fixtures.WebhookService(serviceName, webhookNS) 164 | if _, err := client.CoreV1().Services(webhookNS).Get(ctx, service.Name, metav1.GetOptions{}); err != nil { 165 | _, err = client.CoreV1().Services(webhookNS).Create(ctx, service, metav1.CreateOptions{}) 166 | if err != nil { 167 | return err 168 | } 169 | } 170 | wait.PollUntilContextTimeout(ctx, 10*time.Second, 10*time.Minute, true, func(ctx context.Context) (bool, error) { 171 | deployment, err := client.AppsV1().Deployments(webhookNS).Get(ctx, deploy.Name, metav1.GetOptions{}) 172 | if err != nil { 173 | klog.Errorf("Failed to get deployment: %v", err) 174 | return false, nil 175 | } 176 | if deployment.Status.ReadyReplicas > 0 { 177 | return true, nil 178 | } 179 | klog.Info("Waiting for deployment ready") 180 | return false, nil 181 | }) 182 | return nil 183 | } 184 | -------------------------------------------------------------------------------- /pkg/controller/endpointgroupbinding/controller.go: -------------------------------------------------------------------------------- 1 | package endpointgroupbinding 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/api/errors" 10 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 11 | "k8s.io/apimachinery/pkg/util/wait" 12 | "k8s.io/client-go/informers" 13 | "k8s.io/client-go/kubernetes" 14 | "k8s.io/client-go/kubernetes/scheme" 15 | typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" 16 | corelisters "k8s.io/client-go/listers/core/v1" 17 | networkinglisters "k8s.io/client-go/listers/networking/v1" 18 | "k8s.io/client-go/tools/cache" 19 | "k8s.io/client-go/tools/record" 20 | "k8s.io/client-go/util/workqueue" 21 | "k8s.io/klog/v2" 22 | 23 | endpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis/endpointgroupbinding/v1alpha1" 24 | ownclientset "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned" 25 | ownscheme "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/clientset/versioned/scheme" 26 | owninformers "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/informers/externalversions" 27 | ownlisters "github.com/h3poteto/aws-global-accelerator-controller/pkg/client/listers/endpointgroupbinding/v1alpha1" 28 | ) 29 | 30 | const controllerAgentName = "endpoint-group-binding-controller" 31 | 32 | type EndpointGroupBindingConfig struct { 33 | Workers int 34 | } 35 | 36 | type EndpointGroupBindingController struct { 37 | kubeclient kubernetes.Interface 38 | client ownclientset.Interface 39 | 40 | serviceLister corelisters.ServiceLister 41 | serviceSynced cache.InformerSynced 42 | ingressLister networkinglisters.IngressLister 43 | ingressSynced cache.InformerSynced 44 | endpointGroupBindingLister ownlisters.EndpointGroupBindingLister 45 | endpointGroupBindingSynced cache.InformerSynced 46 | 47 | workqueue workqueue.RateLimitingInterface 48 | recorder record.EventRecorder 49 | } 50 | 51 | // +kubebuilder:rbac:groups=operator.h3poteto.dev,resources=endpointgroupbindings,verbs=get;list;watch;create;update;patch;delete 52 | // +kubebuilder:rbac:groups=operator.h3poteto.dev,resources=endpointgroupbindings/status,verbs=get;update;patch 53 | 54 | func NewEndpointGroupBindingController(kubeclient kubernetes.Interface, ownclientset ownclientset.Interface, informerFactory informers.SharedInformerFactory, ownInformerFactory owninformers.SharedInformerFactory, config *EndpointGroupBindingConfig) *EndpointGroupBindingController { 55 | eventBroadcaster := record.NewBroadcaster() 56 | eventBroadcaster.StartLogging(klog.Infof) 57 | eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclient.CoreV1().Events("")}) 58 | recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName}) 59 | 60 | err := ownscheme.AddToScheme(scheme.Scheme) 61 | if err != nil { 62 | klog.Error(err) 63 | return nil 64 | } 65 | 66 | endpoingGroupBindingInformer := ownInformerFactory.Operator().V1alpha1().EndpointGroupBindings() 67 | 68 | controller := &EndpointGroupBindingController{ 69 | kubeclient: kubeclient, 70 | client: ownclientset, 71 | serviceLister: informerFactory.Core().V1().Services().Lister(), 72 | serviceSynced: informerFactory.Core().V1().Services().Informer().HasSynced, 73 | ingressLister: informerFactory.Networking().V1().Ingresses().Lister(), 74 | ingressSynced: informerFactory.Networking().V1().Ingresses().Informer().HasSynced, 75 | endpointGroupBindingLister: endpoingGroupBindingInformer.Lister(), 76 | endpointGroupBindingSynced: endpoingGroupBindingInformer.Informer().HasSynced, 77 | workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "EndpointGroupBinding"), 78 | recorder: recorder, 79 | } 80 | 81 | klog.Info("Setting up event handlers") 82 | endpoingGroupBindingInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 83 | AddFunc: controller.enqueue, 84 | UpdateFunc: func(old, new interface{}) { 85 | // Changing Spec.EndpointGroupArn field is blocked by ValidatingWebhook 86 | oldEG := old.(*endpointgroupbindingv1alpha1.EndpointGroupBinding) 87 | newEG := new.(*endpointgroupbindingv1alpha1.EndpointGroupBinding) 88 | if oldEG.Spec.EndpointGroupArn != newEG.Spec.EndpointGroupArn { 89 | klog.Error("Do not allow changing EndpointGroupArn field") 90 | return 91 | } 92 | controller.enqueue(new) 93 | }, 94 | }) 95 | 96 | return controller 97 | } 98 | 99 | func (c *EndpointGroupBindingController) Run(threadiness int, stopCh <-chan struct{}) error { 100 | defer utilruntime.HandleCrash() 101 | defer c.workqueue.ShutDown() 102 | 103 | klog.Info("Starting EndpointGroupBinding controller") 104 | 105 | klog.Info("Waiting for informer caches to sync") 106 | if ok := cache.WaitForCacheSync(stopCh, c.endpointGroupBindingSynced, c.serviceSynced, c.ingressSynced); !ok { 107 | return fmt.Errorf("failed to wait for caches to sync") 108 | } 109 | 110 | klog.Info("Starting workers") 111 | for i := 0; i < threadiness; i++ { 112 | go wait.Until(c.runWorker, time.Second, stopCh) 113 | } 114 | 115 | klog.Info("Started workers") 116 | <-stopCh 117 | klog.Info("Shutting down workers") 118 | 119 | return nil 120 | } 121 | 122 | func (c *EndpointGroupBindingController) runWorker() { 123 | for c.processNextWorkItem() { 124 | } 125 | } 126 | 127 | func (c *EndpointGroupBindingController) processNextWorkItem() bool { 128 | key, quit := c.workqueue.Get() 129 | if quit { 130 | return false 131 | } 132 | defer c.workqueue.Done(key) 133 | 134 | err := c.syncHandler(key.(string)) 135 | if err != nil { 136 | utilruntime.HandleError(err) 137 | return true 138 | } 139 | 140 | return true 141 | } 142 | 143 | func (c *EndpointGroupBindingController) syncHandler(key string) error { 144 | ctx := context.Background() 145 | namespace, name, err := cache.SplitMetaNamespaceKey(key) 146 | if err != nil { 147 | utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key)) 148 | return nil 149 | } 150 | 151 | endpointGroupBinding, err := c.endpointGroupBindingLister.EndpointGroupBindings(namespace).Get(name) 152 | if err != nil { 153 | if errors.IsNotFound(err) { 154 | klog.Infof("EndpointGroupBinding %s has been deleted", key) 155 | return nil 156 | } 157 | 158 | return err 159 | } 160 | 161 | res, err := c.reconcile(ctx, endpointGroupBinding) 162 | switch { 163 | case err != nil: 164 | return err 165 | case res.RequeueAfter > 0: 166 | c.workqueue.Forget(key) 167 | c.workqueue.AddAfter(key, res.RequeueAfter) 168 | klog.Infof("Successfully synced %q, but requeued after %v", key, res.RequeueAfter) 169 | case res.Requeue: 170 | c.workqueue.AddRateLimited(key) 171 | klog.Infof("Successfully synced %q, but requeued", key) 172 | default: 173 | c.workqueue.Forget(key) 174 | klog.Infof("Successfully synced %q", key) 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func (c *EndpointGroupBindingController) enqueue(obj interface{}) { 181 | key, err := cache.MetaNamespaceKeyFunc(obj) 182 | if err != nil { 183 | utilruntime.HandleError(err) 184 | return 185 | } 186 | c.workqueue.AddRateLimited(key) 187 | } 188 | -------------------------------------------------------------------------------- /pkg/webhoook/webhook_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/fixture" 12 | admissionv1 "k8s.io/api/admission/v1" 13 | authenicationv1 "k8s.io/api/authentication/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/util/uuid" 17 | ) 18 | 19 | func TestHealthz(t *testing.T) { 20 | reqBody := bytes.NewBufferString("") 21 | req := httptest.NewRequest("GET", "/healthz", reqBody) 22 | response := httptest.NewRecorder() 23 | 24 | Healthz(response, req) 25 | 26 | if response.Code != 200 { 27 | t.Errorf("Expected status code 200, but got %d", response.Code) 28 | } 29 | } 30 | 31 | func TestValidateEndpointGroupBinding_updateWeight(t *testing.T) { 32 | old := fixture.EndpointGroupBinding(false, "example", nil, "arn:aws:globalaccelerator::123456789012:accelerator/1234abcd-abcd-1234-abcd-1234abcd1234") 33 | new := fixture.EndpointGroupBinding(false, "example", aws.Int32(100), "arn:aws:globalaccelerator::123456789012:accelerator/1234abcd-abcd-1234-abcd-1234abcd1234") 34 | rawNew, err := json.Marshal(new) 35 | if err != nil { 36 | t.Errorf("Failed to marshal json: %v", err) 37 | } 38 | rawObjNew := runtime.RawExtension{ 39 | Raw: rawNew, 40 | Object: runtime.Object(&new), 41 | } 42 | rawOld, err := json.Marshal(old) 43 | if err != nil { 44 | t.Errorf("Failed to marshal json: %v", err) 45 | } 46 | rawObjOld := runtime.RawExtension{ 47 | Raw: rawOld, 48 | Object: runtime.Object(&old), 49 | } 50 | 51 | review := admissionv1.AdmissionReview{ 52 | TypeMeta: metav1.TypeMeta{ 53 | Kind: "AdmissionReview", 54 | APIVersion: "admission.k8s.io/v1", 55 | }, 56 | Request: &admissionv1.AdmissionRequest{ 57 | UID: uuid.NewUUID(), 58 | Kind: metav1.GroupVersionKind{ 59 | Group: "operator.h3poteto.dev", 60 | Version: "v1alpha1", 61 | Kind: "EndpointGroupBinding", 62 | }, 63 | Resource: metav1.GroupVersionResource{ 64 | Group: "operator.h3poteto.dev", 65 | Version: "v1alpha1", 66 | Resource: "endpointgroupbindings", 67 | }, 68 | RequestKind: &metav1.GroupVersionKind{ 69 | Group: "operator.h3poteto.dev", 70 | Version: "v1alpha1", 71 | Kind: "EndpointGroupBinding", 72 | }, 73 | RequestResource: &metav1.GroupVersionResource{ 74 | Group: "operator.h3poteto.dev", 75 | Version: "v1alpha1", 76 | Resource: "endpointgroupbindings", 77 | }, 78 | Name: "example", 79 | Namespace: "kube-system", 80 | Operation: admissionv1.Update, 81 | UserInfo: authenicationv1.UserInfo{ 82 | Username: "h3poteto", 83 | UID: string(uuid.NewUUID()), 84 | Groups: []string{}, 85 | Extra: map[string]authenicationv1.ExtraValue{ 86 | "": {}, 87 | }, 88 | }, 89 | Object: rawObjNew, 90 | OldObject: rawObjOld, 91 | DryRun: aws.Bool(false), 92 | }, 93 | } 94 | jbody, err := json.Marshal(review) 95 | if err != nil { 96 | t.Errorf("Failed to marshal json: %v", err) 97 | return 98 | } 99 | reqBody := bytes.NewBuffer(jbody) 100 | req := httptest.NewRequest("POST", "/validate-endpointgroupbinding", reqBody) 101 | req.Header.Set("Content-Type", "application/json") 102 | 103 | response := httptest.NewRecorder() 104 | ValidateEndpointGroupBinding(response, req) 105 | 106 | if response.Code != 200 { 107 | t.Errorf("Expected status code 200, but got %d", response.Code) 108 | return 109 | } 110 | body, err := parseResponse(response.Body) 111 | if err != nil { 112 | t.Errorf("Failed to parse response: %v", err) 113 | } 114 | if !body.Response.Allowed { 115 | t.Errorf("Expected allowed, but got not allowed") 116 | } 117 | if body.Response.Result.Code != http.StatusOK { 118 | t.Errorf("Expected status code 200, but got %d", body.Response.Result.Code) 119 | } 120 | } 121 | 122 | func TestValidateEndpointGroupBinding_updateArn(t *testing.T) { 123 | old := fixture.EndpointGroupBinding(false, "example", nil, "arn:aws:globalaccelerator::123456789012:accelerator/1234abcd-abcd-1234-abcd-1234abcd1234") 124 | new := fixture.EndpointGroupBinding(false, "example", aws.Int32(100), "arn:aws:globalaccelerator::123456789012:accelerator/5678efgh-efgh-5678-efgh-5678efgh5678") 125 | rawNew, err := json.Marshal(new) 126 | if err != nil { 127 | t.Errorf("Failed to marshal json: %v", err) 128 | } 129 | rawObjNew := runtime.RawExtension{ 130 | Raw: rawNew, 131 | Object: runtime.Object(&new), 132 | } 133 | rawOld, err := json.Marshal(old) 134 | if err != nil { 135 | t.Errorf("Failed to marshal json: %v", err) 136 | } 137 | rawObjOld := runtime.RawExtension{ 138 | Raw: rawOld, 139 | Object: runtime.Object(&old), 140 | } 141 | 142 | review := admissionv1.AdmissionReview{ 143 | TypeMeta: metav1.TypeMeta{ 144 | Kind: "AdmissionReview", 145 | APIVersion: "admission.k8s.io/v1", 146 | }, 147 | Request: &admissionv1.AdmissionRequest{ 148 | UID: uuid.NewUUID(), 149 | Kind: metav1.GroupVersionKind{ 150 | Group: "operator.h3poteto.dev", 151 | Version: "v1alpha1", 152 | Kind: "EndpointGroupBinding", 153 | }, 154 | Resource: metav1.GroupVersionResource{ 155 | Group: "operator.h3poteto.dev", 156 | Version: "v1alpha1", 157 | Resource: "endpointgroupbindings", 158 | }, 159 | RequestKind: &metav1.GroupVersionKind{ 160 | Group: "operator.h3poteto.dev", 161 | Version: "v1alpha1", 162 | Kind: "EndpointGroupBinding", 163 | }, 164 | RequestResource: &metav1.GroupVersionResource{ 165 | Group: "operator.h3poteto.dev", 166 | Version: "v1alpha1", 167 | Resource: "endpointgroupbindings", 168 | }, 169 | Name: "example", 170 | Namespace: "kube-system", 171 | Operation: admissionv1.Update, 172 | UserInfo: authenicationv1.UserInfo{ 173 | Username: "h3poteto", 174 | UID: string(uuid.NewUUID()), 175 | Groups: []string{}, 176 | Extra: map[string]authenicationv1.ExtraValue{ 177 | "": {}, 178 | }, 179 | }, 180 | Object: rawObjNew, 181 | OldObject: rawObjOld, 182 | DryRun: aws.Bool(false), 183 | }, 184 | } 185 | jbody, err := json.Marshal(review) 186 | if err != nil { 187 | t.Errorf("Failed to marshal json: %v", err) 188 | return 189 | } 190 | reqBody := bytes.NewBuffer(jbody) 191 | req := httptest.NewRequest("POST", "/validate-endpointgroupbinding", reqBody) 192 | req.Header.Set("Content-Type", "application/json") 193 | 194 | response := httptest.NewRecorder() 195 | ValidateEndpointGroupBinding(response, req) 196 | 197 | if response.Code != 200 { 198 | t.Errorf("Expected status code 200, but got %d", response.Code) 199 | } 200 | body, err := parseResponse(response.Body) 201 | if err != nil { 202 | t.Errorf("Failed to parse response: %v", err) 203 | } 204 | if body.Response.Allowed { 205 | t.Errorf("Expected not allowed, but got allowed") 206 | } 207 | if body.Response.Result.Code != http.StatusForbidden { 208 | t.Errorf("Expected status code 403, but got %d", body.Response.Result.Code) 209 | } 210 | } 211 | 212 | func parseResponse(body *bytes.Buffer) (*admissionv1.AdmissionReview, error) { 213 | var review admissionv1.AdmissionReview 214 | if err := json.Unmarshal(body.Bytes(), &review); err != nil { 215 | return nil, err 216 | } 217 | return &review, nil 218 | } 219 | -------------------------------------------------------------------------------- /pkg/controller/route53/controller.go: -------------------------------------------------------------------------------- 1 | package route53 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "time" 7 | 8 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis" 9 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/reconcile" 10 | 11 | corev1 "k8s.io/api/core/v1" 12 | networkingv1 "k8s.io/api/networking/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 16 | "k8s.io/apimachinery/pkg/util/wait" 17 | "k8s.io/client-go/informers" 18 | "k8s.io/client-go/kubernetes" 19 | typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" 20 | corelisters "k8s.io/client-go/listers/core/v1" 21 | networkinglisters "k8s.io/client-go/listers/networking/v1" 22 | "k8s.io/client-go/tools/cache" 23 | "k8s.io/client-go/tools/record" 24 | "k8s.io/client-go/util/workqueue" 25 | "k8s.io/klog/v2" 26 | "k8s.io/kubectl/pkg/scheme" 27 | ) 28 | 29 | const controllerAgentName = "route53-controller" 30 | 31 | type Route53Config struct { 32 | Workers int 33 | ClusterName string 34 | } 35 | 36 | type Route53Controller struct { 37 | clusterName string 38 | kubeclint kubernetes.Interface 39 | serviceLister corelisters.ServiceLister 40 | serviceSynced cache.InformerSynced 41 | ingressLister networkinglisters.IngressLister 42 | ingressSynced cache.InformerSynced 43 | 44 | serviceQueue workqueue.RateLimitingInterface 45 | ingressQueue workqueue.RateLimitingInterface 46 | 47 | recorder record.EventRecorder 48 | } 49 | 50 | func NewRoute53Controller(kubeclient kubernetes.Interface, informerFactory informers.SharedInformerFactory, config *Route53Config) *Route53Controller { 51 | eventBroadcaster := record.NewBroadcaster() 52 | eventBroadcaster.StartLogging(klog.Infof) 53 | eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclient.CoreV1().Events("")}) 54 | recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName}) 55 | 56 | controller := &Route53Controller{ 57 | clusterName: config.ClusterName, 58 | kubeclint: kubeclient, 59 | recorder: recorder, 60 | serviceQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerAgentName+"-service"), 61 | ingressQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerAgentName+"-ingress"), 62 | } 63 | { 64 | f := informerFactory.Core().V1().Services() 65 | controller.serviceLister = f.Lister() 66 | controller.serviceSynced = f.Informer().HasSynced 67 | f.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 68 | AddFunc: controller.addServiceNotification, 69 | UpdateFunc: controller.updateServiceNotification, 70 | DeleteFunc: controller.deleteServiceNotification, 71 | }) 72 | } 73 | { 74 | f := informerFactory.Networking().V1().Ingresses() 75 | controller.ingressLister = f.Lister() 76 | controller.ingressSynced = f.Informer().HasSynced 77 | f.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 78 | AddFunc: controller.addIngressNotification, 79 | UpdateFunc: controller.updateIngressNotification, 80 | DeleteFunc: controller.deleteIngressNotification, 81 | }) 82 | } 83 | 84 | return controller 85 | } 86 | 87 | func (c *Route53Controller) addServiceNotification(obj interface{}) { 88 | svc := obj.(*corev1.Service) 89 | if wasLoadBalancerService(svc) && hasHostnameAnnotation(svc) { 90 | klog.V(4).Infof("Service %s/%s is created", svc.Namespace, svc.Name) 91 | c.enqueueService(svc) 92 | } 93 | } 94 | 95 | func (c *Route53Controller) updateServiceNotification(old, new interface{}) { 96 | if reflect.DeepEqual(old, new) { 97 | return 98 | } 99 | oldSvc := old.(*corev1.Service) 100 | newSvc := new.(*corev1.Service) 101 | if wasLoadBalancerService(newSvc) { 102 | if hasHostnameAnnotation(newSvc) || hostnameAnnotationChanged(oldSvc, newSvc) { 103 | klog.V(4).Infof("Service %s/%s is updated", newSvc.Namespace, newSvc.Name) 104 | c.enqueueService(newSvc) 105 | } 106 | } 107 | } 108 | 109 | func (c *Route53Controller) deleteServiceNotification(obj interface{}) { 110 | svc, ok := obj.(*corev1.Service) 111 | if !ok { 112 | tombstone, ok := obj.(cache.DeletedFinalStateUnknown) 113 | if !ok { 114 | utilruntime.HandleError(fmt.Errorf("error decoding object, invalid type")) 115 | return 116 | } 117 | svc, ok = tombstone.Obj.(*corev1.Service) 118 | if !ok { 119 | utilruntime.HandleError(fmt.Errorf("error decoding object tombstone, invalid type")) 120 | return 121 | } 122 | klog.V(4).Infof("Recovered deleted object %q from tombstone", svc.Name) 123 | } 124 | if wasLoadBalancerService(svc) { 125 | klog.V(4).Infof("Deleting Service %s/%s", svc.Namespace, svc.Name) 126 | c.enqueueService(svc) 127 | } 128 | } 129 | 130 | func (c *Route53Controller) addIngressNotification(obj interface{}) { 131 | ingress := obj.(*networkingv1.Ingress) 132 | if hasHostnameAnnotation(ingress) { 133 | klog.V(4).Infof("Ingress %s/%s is created", ingress.Namespace, ingress.Name) 134 | c.enqueueIngress(ingress) 135 | } 136 | } 137 | 138 | func (c *Route53Controller) updateIngressNotification(old, new interface{}) { 139 | if reflect.DeepEqual(old, new) { 140 | return 141 | } 142 | oldIngress := old.(*networkingv1.Ingress) 143 | newIngress := new.(*networkingv1.Ingress) 144 | if hasHostnameAnnotation(newIngress) || hostnameAnnotationChanged(oldIngress, newIngress) { 145 | klog.V(4).Infof("Ingress %s/%s is updated", newIngress.Namespace, newIngress.Name) 146 | c.enqueueIngress(newIngress) 147 | } 148 | } 149 | 150 | func (c *Route53Controller) deleteIngressNotification(obj interface{}) { 151 | ingress, ok := obj.(*networkingv1.Ingress) 152 | if !ok { 153 | tombstone, ok := obj.(cache.DeletedFinalStateUnknown) 154 | if !ok { 155 | utilruntime.HandleError(fmt.Errorf("error decoding object, invalid type")) 156 | return 157 | } 158 | ingress, ok = tombstone.Obj.(*networkingv1.Ingress) 159 | if !ok { 160 | utilruntime.HandleError(fmt.Errorf("error decoding object tombstone, invalid type")) 161 | return 162 | } 163 | klog.V(4).Infof("Recovered deleted object %q from tombstone", ingress.Name) 164 | } 165 | c.enqueueIngress(ingress) 166 | } 167 | 168 | func (c *Route53Controller) enqueueService(obj *corev1.Service) { 169 | var key string 170 | var err error 171 | if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { 172 | utilruntime.HandleError(err) 173 | return 174 | } 175 | c.serviceQueue.AddRateLimited(key) 176 | } 177 | 178 | func (c *Route53Controller) enqueueIngress(obj *networkingv1.Ingress) { 179 | var key string 180 | var err error 181 | if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { 182 | utilruntime.HandleError(err) 183 | return 184 | } 185 | c.ingressQueue.AddRateLimited(key) 186 | } 187 | 188 | func (c *Route53Controller) Run(threadiness int, stopCh <-chan struct{}) error { 189 | defer utilruntime.HandleCrash() 190 | defer c.serviceQueue.ShutDown() 191 | defer c.ingressQueue.ShutDown() 192 | 193 | klog.Info("Starting Route53 controller") 194 | 195 | klog.Info("Waiting for informer caches to sync") 196 | if ok := cache.WaitForCacheSync(stopCh, c.serviceSynced); !ok { 197 | return fmt.Errorf("failed to wait for caches to sync") 198 | } 199 | 200 | klog.Info("Starting workers") 201 | for i := 0; i < threadiness; i++ { 202 | go wait.Until(c.runServiceWorker, time.Second, stopCh) 203 | } 204 | for i := 0; i < threadiness; i++ { 205 | go wait.Until(c.runIngressWorker, time.Second, stopCh) 206 | } 207 | 208 | klog.Info("Started workers") 209 | <-stopCh 210 | klog.Info("Shutting down workers") 211 | 212 | return nil 213 | } 214 | 215 | func (c *Route53Controller) runServiceWorker() { 216 | for reconcile.ProcessNextWorkItem(c.serviceQueue, c.keyToService, c.processServiceDelete, c.processServiceCreateOrUpdate) { 217 | } 218 | } 219 | 220 | func (c *Route53Controller) runIngressWorker() { 221 | for reconcile.ProcessNextWorkItem(c.ingressQueue, c.keyToIngress, c.processIngressDelete, c.processIngressCreateOrUpdate) { 222 | } 223 | } 224 | 225 | func (c *Route53Controller) keyToService(key string) (runtime.Object, error) { 226 | ns, name, err := cache.SplitMetaNamespaceKey(key) 227 | if err != nil { 228 | return nil, fmt.Errorf("invalid resource key: %s", key) 229 | } 230 | 231 | return c.serviceLister.Services(ns).Get(name) 232 | } 233 | 234 | func (c *Route53Controller) keyToIngress(key string) (runtime.Object, error) { 235 | ns, name, err := cache.SplitMetaNamespaceKey(key) 236 | if err != nil { 237 | return nil, fmt.Errorf("invalid resource key: %s", key) 238 | } 239 | 240 | return c.ingressLister.Ingresses(ns).Get(name) 241 | } 242 | 243 | func hasHostnameAnnotation(obj metav1.Object) bool { 244 | _, ok := obj.GetAnnotations()[apis.Route53HostnameAnnotation] 245 | return ok 246 | } 247 | 248 | func hostnameAnnotationChanged(old, new metav1.Object) bool { 249 | _, oldHas := old.GetAnnotations()[apis.Route53HostnameAnnotation] 250 | _, newHas := new.GetAnnotations()[apis.Route53HostnameAnnotation] 251 | return oldHas != newHas 252 | } 253 | -------------------------------------------------------------------------------- /pkg/controller/endpointgroupbinding/reconcile.go: -------------------------------------------------------------------------------- 1 | package endpointgroupbinding 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "slices" 7 | "time" 8 | 9 | "github.com/aws/smithy-go" 10 | endpointgroupbindingv1alpha1 "github.com/h3poteto/aws-global-accelerator-controller/pkg/apis/endpointgroupbinding/v1alpha1" 11 | cloudaws "github.com/h3poteto/aws-global-accelerator-controller/pkg/cloudprovider/aws" 12 | "github.com/h3poteto/aws-global-accelerator-controller/pkg/reconcile" 13 | "golang.org/x/exp/maps" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/klog/v2" 16 | ) 17 | 18 | const finalizer = "operator.h3poteto.dev/endpointgroupbindings" 19 | 20 | func (c *EndpointGroupBindingController) reconcile(ctx context.Context, obj *endpointgroupbindingv1alpha1.EndpointGroupBinding) (reconcile.Result, error) { 21 | cloud, err := cloudaws.NewAWS("us-west-2") 22 | if err != nil { 23 | klog.Error(err) 24 | return reconcile.Result{}, err 25 | } 26 | 27 | if obj.DeletionTimestamp != nil { 28 | return c.reconcileDelete(ctx, obj, cloud) 29 | } 30 | if len(obj.Finalizers) == 0 { 31 | return c.reconcileCreate(ctx, obj, cloud) 32 | } 33 | return c.reconcileUpdate(ctx, obj, cloud) 34 | } 35 | 36 | func (c *EndpointGroupBindingController) reconcileDelete(ctx context.Context, obj *endpointgroupbindingv1alpha1.EndpointGroupBinding, cloud *cloudaws.AWS) (reconcile.Result, error) { 37 | if len(obj.Status.EndpointIds) == 0 { 38 | copied := obj.DeepCopy() 39 | copied.Finalizers = []string{} 40 | _, err := c.client.OperatorV1alpha1().EndpointGroupBindings(copied.Namespace).Update(ctx, copied, metav1.UpdateOptions{}) 41 | if err != nil { 42 | klog.Error(err) 43 | return reconcile.Result{}, err 44 | } 45 | return reconcile.Result{}, nil 46 | } 47 | 48 | endpoint, err := cloud.DescribeEndpointGroup(ctx, obj.Spec.EndpointGroupArn) 49 | if err != nil { 50 | // If the endpoint group is not found, we should remove the finalizer and update the object. 51 | var awsErr smithy.APIError 52 | if errors.As(err, &awsErr) { 53 | klog.V(1).Infof("Failed to get EndpointGroup %s: %s", obj.Spec.EndpointGroupArn, awsErr.ErrorCode()) 54 | if awsErr.ErrorCode() == cloudaws.ErrEndpointGroupNotFoundException { 55 | copied := obj.DeepCopy() 56 | copied.Finalizers = []string{} 57 | _, err := c.client.OperatorV1alpha1().EndpointGroupBindings(copied.Namespace).Update(ctx, copied, metav1.UpdateOptions{}) 58 | if err != nil { 59 | klog.Error(err) 60 | return reconcile.Result{}, err 61 | } 62 | return reconcile.Result{}, nil 63 | } 64 | } 65 | 66 | klog.Error(err) 67 | return reconcile.Result{}, err 68 | 69 | } 70 | endpointIds := obj.Status.EndpointIds 71 | for i := range obj.Status.EndpointIds { 72 | id := obj.Status.EndpointIds[i] 73 | region := cloudaws.GetRegionFromARN(id) 74 | cloud, err := cloudaws.NewAWS(region) 75 | if err != nil { 76 | klog.Error(err) 77 | return reconcile.Result{}, err 78 | } 79 | 80 | err = cloud.RemoveLBFromEdnpointGroup(ctx, endpoint, id) 81 | if err != nil { 82 | return reconcile.Result{}, err 83 | } 84 | endpointIds = append(endpointIds[:i], endpointIds[i+1:]...) 85 | } 86 | 87 | copied := obj.DeepCopy() 88 | copied.Status.EndpointIds = endpointIds 89 | copied.Status.ObservedGeneration = obj.Generation 90 | _, err = c.client.OperatorV1alpha1().EndpointGroupBindings(copied.Namespace).UpdateStatus(ctx, copied, metav1.UpdateOptions{}) 91 | if err != nil { 92 | klog.Error(err) 93 | return reconcile.Result{}, err 94 | } 95 | 96 | return reconcile.Result{Requeue: true, RequeueAfter: 1 * time.Second}, nil 97 | } 98 | 99 | func (c *EndpointGroupBindingController) reconcileCreate(ctx context.Context, obj *endpointgroupbindingv1alpha1.EndpointGroupBinding, _ *cloudaws.AWS) (reconcile.Result, error) { 100 | copied := obj.DeepCopy() 101 | copied.Finalizers = []string{finalizer} 102 | 103 | _, err := c.client.OperatorV1alpha1().EndpointGroupBindings(copied.Namespace).Update(ctx, copied, metav1.UpdateOptions{}) 104 | if err != nil { 105 | klog.Error(err) 106 | return reconcile.Result{}, err 107 | } 108 | 109 | return reconcile.Result{}, nil 110 | } 111 | 112 | func (c *EndpointGroupBindingController) reconcileUpdate(ctx context.Context, obj *endpointgroupbindingv1alpha1.EndpointGroupBinding, cloud *cloudaws.AWS) (reconcile.Result, error) { 113 | // Check the different between the current service/ingress load balancer ARNs and status.endpointIds 114 | // If the ARN is not in the status.endpointIds, add it to the endpoint group 115 | // If status.endpointIds is not in the ARN, remove it from the endpoint group 116 | 117 | arns := map[string]string{} 118 | hostnames, err := c.getLoadBalancerHostName(obj) 119 | if err != nil { 120 | return reconcile.Result{}, err 121 | } 122 | var regionalCloud *cloudaws.AWS 123 | for _, hostname := range hostnames { 124 | name, region, err := cloudaws.GetLBNameFromHostname(hostname) 125 | if err != nil { 126 | klog.Error(err) 127 | return reconcile.Result{}, err 128 | } 129 | regionalCloud, err = cloudaws.NewAWS(region) 130 | if err != nil { 131 | klog.Error(err) 132 | return reconcile.Result{}, err 133 | } 134 | lb, err := regionalCloud.GetLoadBalancer(ctx, name) 135 | if err != nil { 136 | klog.Error(err) 137 | return reconcile.Result{}, err 138 | } 139 | arns[*lb.LoadBalancerArn] = name 140 | } 141 | klog.V(4).Infof("Service LoadBalancer ARNs: %v", arns) 142 | 143 | newEndpointIds := []string{} 144 | removedEndpointIds := []string{} 145 | for arn, _ := range arns { 146 | if !slices.Contains(obj.Status.EndpointIds, arn) { 147 | newEndpointIds = append(newEndpointIds, arn) 148 | } 149 | } 150 | for _, endpointId := range obj.Status.EndpointIds { 151 | if !slices.Contains(maps.Keys(arns), endpointId) { 152 | removedEndpointIds = append(removedEndpointIds, endpointId) 153 | } 154 | } 155 | klog.V(4).Infof("New EndpointIds: %v", newEndpointIds) 156 | klog.V(4).Infof("Removed EndpointIds: %v", removedEndpointIds) 157 | if len(newEndpointIds) == 0 && len(removedEndpointIds) == 0 && obj.Status.ObservedGeneration == obj.Generation { 158 | return reconcile.Result{}, nil 159 | } 160 | 161 | endpointGroup, err := cloud.DescribeEndpointGroup(ctx, obj.Spec.EndpointGroupArn) 162 | if err != nil { 163 | klog.Error(err) 164 | return reconcile.Result{}, err 165 | } 166 | 167 | results := obj.Status.EndpointIds 168 | 169 | for _, endpointId := range removedEndpointIds { 170 | err := regionalCloud.RemoveLBFromEdnpointGroup(ctx, endpointGroup, endpointId) 171 | if err != nil { 172 | klog.Error(err) 173 | return reconcile.Result{}, err 174 | } 175 | results = slices.DeleteFunc(results, func(e string) bool { 176 | return e == endpointId 177 | }) 178 | } 179 | 180 | for _, endpointId := range newEndpointIds { 181 | id, retry, err := regionalCloud.AddLBToEndpointGroup(ctx, endpointGroup, arns[endpointId], obj.Spec.ClientIPPreservation, obj.Spec.Weight) 182 | if err != nil { 183 | klog.Error(err) 184 | return reconcile.Result{}, err 185 | } 186 | if retry > 0 { 187 | return reconcile.Result{ 188 | Requeue: true, 189 | RequeueAfter: retry, 190 | }, nil 191 | } 192 | if id != nil { 193 | results = append(results, *id) 194 | } 195 | } 196 | 197 | // Check weight of the endpoint 198 | for id, _ := range arns { 199 | err := regionalCloud.UpdateEndpointWeight(ctx, endpointGroup, id, obj.Spec.Weight) 200 | if err != nil { 201 | klog.Error(err) 202 | return reconcile.Result{}, err 203 | } 204 | } 205 | 206 | copied := obj.DeepCopy() 207 | copied.Status.EndpointIds = results 208 | copied.Status.ObservedGeneration = obj.Generation 209 | _, err = c.client.OperatorV1alpha1().EndpointGroupBindings(copied.Namespace).UpdateStatus(ctx, copied, metav1.UpdateOptions{}) 210 | 211 | if err != nil { 212 | klog.Error(err) 213 | return reconcile.Result{}, err 214 | } 215 | 216 | return reconcile.Result{}, nil 217 | } 218 | 219 | func (c *EndpointGroupBindingController) getLoadBalancerHostName(obj *endpointgroupbindingv1alpha1.EndpointGroupBinding) ([]string, error) { 220 | hostnames := []string{} 221 | if obj.Spec.ServiceRef != nil { 222 | service, err := c.serviceLister.Services(obj.Namespace).Get(obj.Spec.ServiceRef.Name) 223 | if err != nil { 224 | klog.Error(err) 225 | return []string{}, err 226 | } 227 | if len(service.Status.LoadBalancer.Ingress) < 1 { 228 | klog.Warningf("%s/%s does not have ingress LoadBalancer, so skip it", service.Namespace, service.Name) 229 | return []string{}, nil 230 | } 231 | for i := range service.Status.LoadBalancer.Ingress { 232 | hostnames = append(hostnames, service.Status.LoadBalancer.Ingress[i].Hostname) 233 | } 234 | } else if obj.Spec.IngressRef != nil { 235 | ingress, err := c.ingressLister.Ingresses(obj.Namespace).Get(obj.Spec.IngressRef.Name) 236 | if err != nil { 237 | klog.Error(err) 238 | return []string{}, err 239 | } 240 | if len(ingress.Status.LoadBalancer.Ingress) < 1 { 241 | klog.Warningf("%s/%s does not have ingress LoadBalancer, so skip it", ingress.Namespace, ingress.Name) 242 | return []string{}, nil 243 | } 244 | for i := range ingress.Status.LoadBalancer.Ingress { 245 | hostnames = append(hostnames, ingress.Status.LoadBalancer.Ingress[i].Hostname) 246 | } 247 | } else { 248 | klog.Errorf("EndpointGroupBinding %s does not have serviceRef or ingressRef", obj.Name) 249 | return []string{}, nil 250 | } 251 | return hostnames, nil 252 | } 253 | --------------------------------------------------------------------------------