Skip to content

Commit

Permalink
feat: support google artifact registry as target
Browse files Browse the repository at this point in the history
  • Loading branch information
kjvellajr committed Feb 1, 2023
1 parent 0a01de7 commit d80dca9
Show file tree
Hide file tree
Showing 16 changed files with 563 additions and 108 deletions.
24 changes: 21 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,12 @@ A mutating webhook for Kubernetes, pointing the images to a new location.`,
//metricsRec := metrics.NewPrometheus(promReg)
log.Trace().Interface("config", cfg).Msg("config")

rClient, err := registry.NewECRClient(cfg.Target.AWS.Region, cfg.Target.AWS.EcrDomain(), cfg.Target.AWS.AccountID, cfg.Target.AWS.Role, cfg.Target.AWS.ECROptions.AccessPolicy, cfg.Target.AWS.ECROptions.LifecyclePolicy)
rClient, err := setupTargetRegistryClient()
if err != nil {
log.Err(err).Msg("error connecting to registry client")
os.Exit(1)
}

rClient.SetRepositoryTags(cfg.Target.AWS.ECROptions.Tags)

imageSwapPolicy, err := types.ParseImageSwapPolicy(cfg.ImageSwapPolicy)
if err != nil {
log.Err(err).Str("policy", cfg.ImageSwapPolicy).Msg("parsing image swap policy failed")
Expand Down Expand Up @@ -206,6 +204,9 @@ func init() {

// initConfig reads in config file and ENV variables if set.
func initConfig() {
// Default to aws target registry type if none are defined
viper.SetDefault("target.type", "aws")

if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
Expand Down Expand Up @@ -278,3 +279,20 @@ func setupImagePullSecretsProvider() secrets.ImagePullSecretsProvider {

return secrets.NewKubernetesImagePullSecretsProvider(clientset)
}

// setupRegistry configures a target registry client connection
func setupTargetRegistryClient() (registry.Client, error) {
targetRegistry, err := types.ParseTargetRegistry(cfg.Target.Type)
if err != nil {
log.Err(err)
}

switch targetRegistry {
case types.TargetRegistryAws:
return registry.NewECRClient(cfg.Target.AWS)
case types.TargetRegistryGcp:
return registry.NewGARClient(cfg.Target.GCP)
}

return nil, fmt.Errorf("no registry for target registry type: '%s'", targetRegistry)
}
16 changes: 16 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ The AWS Account ID and Region is primarily used to construct the ECR domain `[AC
!!! example
```yaml
target:
type: aws
aws:
accountId: 123456789
region: ap-southeast-2
Expand All @@ -161,3 +162,18 @@ It's a slice of `Key` and `Value`.
- key: cluster
value: myCluster
```

### GCP

The option `target.registry.gcp` holds details about the target registry storing the images.
The GCP location, projectId, and repositoryId are used to constrct the GCP Artifact Registry domain `[LOCATION]-docker.pkg.dev/[PROJECT_ID]/[REPOSITORY_ID]`.

!!! example
```yaml
target:
type: gcp
gcp:
location: us-central1
projectId: gcp-project-123
repositoryId: main
```
146 changes: 145 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This document will provide guidance for installing `k8s-image-swapper`.
## Prerequisites

`k8s-image-swapper` will automatically create image repositories and mirror images into them.
This requires certain permissions for your target registry (_only AWS ECR supported atm_).
This requires certain permissions for your target registry (_only AWS ECR and GCP ArtifactRegistry are supported atm_).

Before you get started choose a namespace to install `k8s-image-swapper` in, e.g. `operations` or `k8s-image-swapper`.
Ensure the namespace exists and is configured as your current context[^1].
Expand Down Expand Up @@ -57,6 +57,7 @@ You can specify the access policy that will be applied to the created repos in c
For example:
```yaml
target:
type: aws
aws:
accountId: 123456789
region: ap-southeast-2
Expand Down Expand Up @@ -92,6 +93,7 @@ Similarly to access policy, lifecycle policy can be specified, for example:

```yaml
target:
type: aws
aws:
accountId: 123456789
region: ap-southeast-2
Expand Down Expand Up @@ -142,6 +144,147 @@ target:
Note: You can see a complete example below in [Terraform](Terraform)
### GCP Artifact Registry as a target registry
To target a GCP Artifact Registry set the `target.type` to `gcp` and provide additional metadata in the configuration.
```yaml
target:
type: gcp
gcp:
location: us-central1
projectId: gcp-project-123
repositoryId: main
```

!!! note
This is fundamentally different from the AWS ECR implementation since all images will be stored under *one* GCP Artifact Registry repository.
<p align="center">
<img alt="GCP Console - Artifact Registry" src="img/gcp_artifact_registry.png" />
</p>


#### Create Repository

Create and configure a single GCP Artifact Registry repository to store Docker images for `k8s-iamge-swapper`.

```hcl
resource "google_artifact_registry_repository" "repo" {
project = var.project_id
location = var.region
repository_id = "main"
description = "main docker repository"
format = "DOCKER"
}
```

#### IAM for GKE / Nodes / Compute

Give the compute service account that the nodes use, permissions to pull images from Artifact Registry

```hcl
resource "google_project_iam_member" "compute_artifactregistry_reader" {
project = var.project_id
role = "roles/artifactregistry.reader"
member = "serviceAccount:${var.compute_sa_email}"
}
```

Allow GKE node pools to access Artifact Registry API via oauth scope `https://www.googleapis.com/auth/devstorage.read_only`

```hcl
resource "google_container_node_pool" "primary_nodes_v1" {
project = var.project_id
name = "${google_container_cluster.primary.name}-node-pool-v1"
location = var.region
cluster = google_container_cluster.primary.name
...
node_config {
oauth_scopes = [
...
"https://www.googleapis.com/auth/devstorage.read_only",
]
...
}
...
}
```

#### IAM for `k8s-image-swapper`

On GKE, leverage Workload Identity for the `k8s-image-swapper` K8s service account

1. Enable Workload Identity on the GKE cluster.
https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity
```hcl
resource "google_container_cluster" "primary" {
...
workload_identity_config {
workload_pool = "${var.project_id}.svc.id.goog"
}
...
}
```
2. Setup a Google Service Account (GSA) for `k8s-image-swapper`.
```hcl
resource "google_service_account" "k8s_image_swapper_service_account" {
project = var.project_id
account_id = k8s-image-swapper
display_name = "Workload identity for kube-system/k8s-image-swapper"
}
```
3. Setup Workload Identity for the GSA
This example assumes `k8s-image-swapper` is deployed to the `kube-system` namespace and uses `k8s-image-swapper` as the K8s service account name.
```hcl
resource "google_service_account_iam_member" "k8s_image_swapper_workload_identity_binding" {
service_account_id = google_service_account.k8s_image_swapper_service_account.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[kube-system/k8s-image-swapper]"
depends_on = [
google_container_cluster.primary,
]
}
```
4. Bind permissions for GSA to access Artifact Registry
We setup the `roles/artifactregistry.writer` role so that `k8s-image-swapper` can read/write images to our Artifact Repository
```hcl
resource "google_project_iam_member" "k8s_image_swapper_service_account_binding" {
project = var.project_id
role = "roles/artifactregistry.writer"
member = "serviceAccount:${google_service_account.k8s_image_swapper_service_account.email}"
}
```
5. (Optional) Bind additional permissions for GSA to read from other GCP Artifact Registries
6. Set Workload Identity annotation on `k8s-iamge-swapper` service account
```yaml
serviceAccount:
annotations:
iam.gke.io/gcp-service-account: k8s-image-swapper@gcp-project-123.iam.gserviceaccount.com
```
#### Firewall
If running `k8s-image-swapper` on a private GKE cluster you must have a firewall rule enabled to allow the GKE control plane to talk to `k8s-image-swapper` on port `8443`. See the following Terraform example for the firewall configuration.

```hcl
resource "google_compute_firewall" "k8s_image_swapper_webhook" {
project = var.project_id
name = "gke-${google_container_cluster.primary.name}-k8s-image-swapper-webhook"
network = google_compute_network.vpc.name
direction = "INGRESS"
source_ranges = [google_container_cluster.primary.private_cluster_config[0].master_ipv4_cidr_block]
target_tags = [google_container_cluster.primary.name]
allow {
ports = ["8443"]
protocol = "tcp"
}
}
```

For more details see https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters#add_firewall_rules

## Helm

1. Add the Helm chart repository:
Expand Down Expand Up @@ -270,6 +413,7 @@ config:
- jmespath: "obj.metadata.namespace != 'default'"
- jmespath: "contains(container.image, '.dkr.ecr.') && contains(container.image, '.amazonaws.com')"
target:
type: aws
aws:
accountId: "${data.aws_caller_identity.current.account_id}"
region: ${var.region}
Expand Down
Binary file added docs/img/gcp_artifact_registry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/estahn/k8s-image-swapper
go 1.18

require (
cloud.google.com/go/artifactregistry v1.10.0
github.com/alitto/pond v1.8.2
github.com/aws/aws-sdk-go v1.44.189
github.com/containers/image/v5 v5.24.0
Expand All @@ -20,6 +21,7 @@ require (
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.1
google.golang.org/api v0.107.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.26.1
k8s.io/apimachinery v0.26.1
Expand All @@ -32,6 +34,7 @@ require (
cloud.google.com/go/compute v1.14.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.8.0 // indirect
cloud.google.com/go/longrunning v0.3.0 // indirect
cloud.google.com/go/storage v1.27.0 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
Expand Down Expand Up @@ -152,7 +155,6 @@ require (
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
gomodules.xyz/jsonpatch/v3 v3.0.1 // indirect
gomodules.xyz/orderedmap v0.1.0 // indirect
google.golang.org/api v0.107.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect
google.golang.org/grpc v1.52.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
cloud.google.com/go/artifactregistry v1.10.0 h1:bXkHmrLC3yl+mlk+SdCHSp584KW9HObbaRNdPlM/RyA=
cloud.google.com/go/artifactregistry v1.10.0/go.mod h1:zuY01UrUIc2Hoxlm4O9mMOm5prnOZeGuyARawQO9BFU=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
Expand All @@ -35,6 +37,7 @@ cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1
cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk=
cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE=
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
Expand Down
10 changes: 9 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ type JMESPathFilter struct {
}

type Target struct {
AWS AWS `yaml:"aws"`
Type string `yaml:"type"`
AWS AWS `yaml:"aws"`
GCP GCP `yaml:"gcp"`
}

type AWS struct {
Expand Down Expand Up @@ -91,3 +93,9 @@ type EncryptionConfiguration struct {
func (a *AWS) EcrDomain() string {
return fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", a.AccountID, a.Region)
}

type GCP struct {
Location string `yaml:"location"`
ProjectID string `yaml:"projectId"`
RepositoryID string `yaml:"repositoryId"`
}
1 change: 1 addition & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ target:
`,
expCfg: Config{
Target: Target{
Type: "aws",
AWS: AWS{
AccountID: "123456789",
Region: "ap-southeast-2",
Expand Down
10 changes: 7 additions & 3 deletions pkg/registry/client.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package registry

import "context"
import (
"context"

ctypes "github.com/containers/image/v5/types"
)

// Client provides methods required to be implemented by the various target registry clients, e.g. ECR, Docker, Quay.
type Client interface {
CreateRepository(ctx context.Context, name string) error
RepositoryExists() bool
CopyImage() error
CopyImage(ctx context.Context, src ctypes.ImageReference, srcCreds string, dest ctypes.ImageReference, destCreds string) error
PullImage() error
PutImage() error
ImageExists(ctx context.Context, ref string) bool
ImageExists(ctx context.Context, ref ctypes.ImageReference) bool

// Endpoint returns the domain of the registry
Endpoint() string
Expand Down
Loading

0 comments on commit d80dca9

Please sign in to comment.