diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09427ce..27fb295 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,18 @@ name: CI on: push: branches: - - "**" + - main + tags: + - "v*" pull_request: + branches: + - main workflow_dispatch: +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: tests: name: Python ${{ matrix.python-version }} on ${{ matrix.os }} @@ -55,29 +63,47 @@ jobs: docker-build-push: name: Build & Push Docker Image runs-on: ubuntu-latest + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) needs: tests permissions: contents: 'read' - id-token: 'write' + env: + ACR_NAME: gabby + REGISTRY: gabby.azurecr.io + IMAGE_NAME: python-dsa steps: - name: Checkout uses: actions/checkout@v4 - - id: auth - uses: google-github-actions/auth@v2 + - name: Azure Login + uses: azure/login@v2 with: - workload_identity_provider: projects/316518955652/locations/global/workloadIdentityPools/github-pool/providers/github-oidc - service_account: kame-house-oidc@kame-457417.iam.gserviceaccount.com - create_credentials_file: true - export_environment_variables: true + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to Azure Container Registry + run: az acr login --name $ACR_NAME - - name: Configure Docker for Artifact Registry - run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha + type=raw,value=latest,enable={{is_default_branch}} - name: Build and Push Docker image uses: docker/build-push-action@v5 with: context: . push: true - tags: us-central1-docker.pkg.dev/kame-457417/python-dsa/python-dsa:latest + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/docs/GITHUB_ACTIONS_NOTES.md b/docs/GITHUB_ACTIONS_NOTES.md index 6b51df8..6a35831 100644 --- a/docs/GITHUB_ACTIONS_NOTES.md +++ b/docs/GITHUB_ACTIONS_NOTES.md @@ -1,14 +1,19 @@ # GitHub Actions Workflow Notes ## CI Workflow Overview + The repository uses a GitHub Actions workflow defined in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml). ### Triggers -- Runs on **all branches** (`push`). -- Runs on **pull requests**. -- Can be triggered manually via **workflow_dispatch**. + +- `push` on the **`main` branch** and version tags (`v*`). +- `pull_request` targeting **`main`**. +- Manual runs via **workflow_dispatch** when needed. + +> The workflow also uses `concurrency` to ensure only the latest run per ref/PR stays active, which keeps pending check counts low. ### Jobs + 1. **Tests** - Runs on Python 3.10, 3.11, and 3.12. - Installs dependencies with pip. @@ -17,28 +22,32 @@ The repository uses a GitHub Actions workflow defined in [`.github/workflows/ci. - Uploads `coverage.xml` as an artifact. 2. **Docker Build & Push** - - Runs after tests succeed. - - Authenticates to Google Cloud using **Workload Identity Federation (WIF)**: - - Workload Identity Provider: - `projects/316518955652/locations/global/workloadIdentityPools/github-pool/providers/github-oidc` - - Service Account: - `kame-house-oidc@kame-457417.iam.gserviceaccount.com` - - Configures Docker for Artifact Registry. - - Builds and pushes the Docker image to: - `us-central1-docker.pkg.dev/kame-457417/python-dsa/python-dsa:latest` - -### Workload Identity Federation (WIF) Setup -- The service account is bound with: + +- Runs only for `push` events on `main` (or tagged releases) after tests succeed. +- Authenticates to Azure using [`azure/login`](https://github.com/Azure/login) with the `AZURE_CREDENTIALS` secret (JSON output from `az ad sp create-for-rbac --sdk-auth`). +- Logs in to Azure Container Registry (ACR) `gabby`. +- Uses `docker/setup-buildx-action` + `docker/metadata-action` for reproducible builds with cache reuse. +- Builds and pushes the Docker image to `gabby.azurecr.io/python-dsa` with tags: `latest` (default branch), branch/tag refs, and the commit SHA. + +### Azure Service Principal Setup + +- **Recommended**: configure GitHub OIDC federation with Azure AD so the workflow can exchange short-lived tokens without storing long-lived client secrets. This avoids the deprecated `--sdk-auth` flow and removes the need for the `AZURE_CREDENTIALS` secret. Follow the [federated credential guide](https://learn.microsoft.com/azure/active-directory/develop/workload-identity-federation-create-trust-github) to create an app registration, add a federated credential for your repo, and assign the `AcrPush` role to the app. + +- **Legacy fallback**: if you must keep using a client secret, create (or reuse) a service principal with `AcrPush` permissions on the `gabby` registry and store the JSON output as the `AZURE_CREDENTIALS` secret. Note that `--sdk-auth` is deprecated and may be removed in future CLI versions, so migrate when possible. + ```bash - gcloud iam service-accounts add-iam-policy-binding \ - ${{ GCP_SERVICE_ACCOUNT }} \ - --role="roles/iam.workloadIdentityUser" \ - --member="principalSet://iam.googleapis.com/projects/${{ GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/${{ GCP_WIF_POOL }}/attribute.repository/ianlintner/python_dsa" + az ad sp create-for-rbac \ + --name python-dsa-github-ci \ + --role AcrPush \ + --scopes /subscriptions/79307c77-54c3-4738-be2a-dc96da7464d9/resourceGroups/nekoc/providers/Microsoft.ContainerRegistry/registries/gabby \ + --sdk-auth ``` ### Notes -- If you encounter `unauthorized_client` errors, check the **attribute condition** on the WIF provider in GCP. -- Ensure the service account has the correct roles (e.g., `roles/artifactregistry.writer`). + +- If you encounter `AADSTS700016`/`invalid_client` errors, confirm the JSON in `AZURE_CREDENTIALS` matches the service principal used for `azure/login`. +- The service principal must have at least `AcrPush` on the target registry. - Use `workflow_dispatch` for manual runs: + ```bash gh workflow run CI --ref main diff --git a/k8s/base/auth.yaml b/k8s/base/auth.yaml deleted file mode 100644 index b2417c0..0000000 --- a/k8s/base/auth.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: security.istio.io/v1beta1 -kind: AuthorizationPolicy -metadata: - name: python-dsa-policy - namespace: prod -spec: - action: ALLOW - rules: - - to: - - operation: - methods: ["*"] diff --git a/k8s/base/deployment.yaml b/k8s/base/deployment.yaml deleted file mode 100644 index 9701a7d..0000000 --- a/k8s/base/deployment.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: python-dsa - labels: - app: python-dsa -spec: - replicas: 1 - selector: - matchLabels: - app: python-dsa - template: - metadata: - labels: - app: python-dsa - istio-injection: enabled - annotations: - sidecar.istio.io/inject: "true" - spec: - containers: - - name: python-dsa - image: us-central1-docker.pkg.dev/kame-457417/python-dsa/python-dsa:latest - imagePullPolicy: Always - ports: - - containerPort: 5000 - resources: - requests: - cpu: "100m" - memory: "128Mi" - limits: - cpu: "500m" - memory: "256Mi" diff --git a/k8s/base/dns.yaml b/k8s/base/dns.yaml deleted file mode 100644 index e0bb202..0000000 --- a/k8s/base/dns.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: dns.cnrm.cloud.google.com/v1beta1 -kind: DNSRecordSet -metadata: - name: hugecat-net-dsa-a - namespace: config-control - annotations: - cnrm.cloud.google.com/management-conflict-prevention-policy: none - cnrm.cloud.google.com/project-id: kame-457417 - cnrm.cloud.google.com/state-into-spec: absent -spec: - name: "dsa.hugecat.net." - type: "A" - ttl: 300 - managedZoneRef: - name: hugecat-net - rrdatas: - - "34.31.128.59" diff --git a/k8s/base/istio-certificate.yaml b/k8s/base/istio-certificate.yaml deleted file mode 100644 index 9a1c00b..0000000 --- a/k8s/base/istio-certificate.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: python-dsa-tls-cert - labels: - app: python-dsa -spec: - secretName: python-dsa-tls-cert - issuerRef: - name: letsencrypt-prod - kind: ClusterIssuer - dnsNames: - - dsa.hugecat.net diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml deleted file mode 100644 index 26432f3..0000000 --- a/k8s/base/kustomization.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - deployment.yaml - - dns.yaml - - service.yaml - - istio-gateway.yaml - - istio-virtualservice.yaml - - istio-certificate.yaml - - auth.yaml - - serviceaccount.yaml diff --git a/k8s/base/service.yaml b/k8s/base/service.yaml deleted file mode 100644 index 0ddcd49..0000000 --- a/k8s/base/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: python-dsa - labels: - app: python-dsa -spec: - selector: - app: python-dsa - ports: - - protocol: TCP - port: 80 - targetPort: 5000 - - type: ClusterIP diff --git a/k8s/base/serviceaccount.yaml b/k8s/base/serviceaccount.yaml deleted file mode 100644 index 9ae0a91..0000000 --- a/k8s/base/serviceaccount.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: python-dsa-workload-identity - annotations: - iam.gke.io/gcp-service-account: python-dsa-cloudsql-sa@kame-457417.iam.gserviceaccount.com diff --git a/k8s/overlays/azure/app.yaml b/k8s/overlays/azure/app.yaml new file mode 100644 index 0000000..1af0b39 --- /dev/null +++ b/k8s/overlays/azure/app.yaml @@ -0,0 +1,77 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: python-dsa-workload-identity + labels: + app: python-dsa +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: python-dsa + labels: + app: python-dsa +spec: + replicas: 1 + selector: + matchLabels: + app: python-dsa + template: + metadata: + labels: + app: python-dsa + annotations: + sidecar.istio.io/inject: "true" + spec: + serviceAccountName: python-dsa-workload-identity + containers: + - name: python-dsa + image: gabby.azurecr.io/python-dsa:latest + imagePullPolicy: Always + ports: + - containerPort: 5000 + env: + - name: FLASK_ENV + value: production + - name: PYTHONUNBUFFERED + value: "1" + - name: GUNICORN_WORKERS + value: "4" + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" +--- +apiVersion: v1 +kind: Service +metadata: + name: python-dsa + labels: + app: python-dsa +spec: + selector: + app: python-dsa + ports: + - protocol: TCP + port: 80 + targetPort: 5000 + type: ClusterIP +--- +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: python-dsa-policy + namespace: default +spec: + selector: + matchLabels: + app: python-dsa + action: ALLOW + rules: + - to: + - operation: + methods: + - "*" diff --git a/k8s/overlays/azure/certificate-dsa-cat-herding.yaml b/k8s/overlays/azure/certificate-dsa-cat-herding.yaml new file mode 100644 index 0000000..7a3cd91 --- /dev/null +++ b/k8s/overlays/azure/certificate-dsa-cat-herding.yaml @@ -0,0 +1,21 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: python-dsa-tls-cert + namespace: default +spec: + secretName: python-dsa-tls-cert + dnsNames: + - dsa.cat-herding.net + # Optionally include legacy host if you want a combined cert: + # - dsa.hugecat.net + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + privateKey: + rotationPolicy: Always + algorithm: RSA + size: 2048 + usages: + - digital signature + - key encipherment diff --git a/k8s/base/istio-gateway.yaml b/k8s/overlays/azure/gateway-default.yaml similarity index 70% rename from k8s/base/istio-gateway.yaml rename to k8s/overlays/azure/gateway-default.yaml index 34c17e2..9edc879 100644 --- a/k8s/base/istio-gateway.yaml +++ b/k8s/overlays/azure/gateway-default.yaml @@ -1,18 +1,17 @@ -apiVersion: networking.istio.io/v1beta1 +apiVersion: networking.istio.io/v1 kind: Gateway metadata: name: python-dsa-gateway - namespace: prod spec: selector: - istio: ingressgateway + istio: aks-istio-ingressgateway-external servers: - port: number: 443 name: https protocol: HTTPS hosts: - - "dsa.hugecat.net" + - dsa.cat-herding.net tls: mode: SIMPLE credentialName: python-dsa-tls-cert @@ -21,4 +20,4 @@ spec: name: http protocol: HTTP hosts: - - "dsa.hugecat.net" + - dsa.cat-herding.net diff --git a/k8s/overlays/azure/kustomization.yaml b/k8s/overlays/azure/kustomization.yaml new file mode 100644 index 0000000..7e097b2 --- /dev/null +++ b/k8s/overlays/azure/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Use default namespace; cluster-scoped resources only. +# Remove explicit namespace to avoid creating a new one. + +resources: + - app.yaml + - certificate-dsa-cat-herding.yaml + - gateway-default.yaml + - virtualservice-default.yaml diff --git a/k8s/base/istio-virtualservice.yaml b/k8s/overlays/azure/virtualservice-default.yaml similarity index 77% rename from k8s/base/istio-virtualservice.yaml rename to k8s/overlays/azure/virtualservice-default.yaml index 50b6cc7..92f714b 100644 --- a/k8s/base/istio-virtualservice.yaml +++ b/k8s/overlays/azure/virtualservice-default.yaml @@ -1,11 +1,10 @@ -apiVersion: networking.istio.io/v1beta1 +apiVersion: networking.istio.io/v1 kind: VirtualService metadata: name: python-dsa-virtualservice - namespace: prod spec: hosts: - - "dsa.hugecat.net" + - dsa.cat-herding.net gateways: - python-dsa-gateway http: diff --git a/k8s/overlays/prod/artifact-registry-iam.yaml b/k8s/overlays/prod/artifact-registry-iam.yaml deleted file mode 100644 index 4ea056d..0000000 --- a/k8s/overlays/prod/artifact-registry-iam.yaml +++ /dev/null @@ -1,56 +0,0 @@ -apiVersion: iam.cnrm.cloud.google.com/v1beta1 -kind: IAMPolicyMember -metadata: - name: artifact-registry-writer -spec: - member: serviceAccount:kame-house-oidc@kame-457417.iam.gserviceaccount.com - role: roles/artifactregistry.writer - resourceRef: - apiVersion: resourcemanager.cnrm.cloud.google.com/v1beta1 - kind: Project - external: kame-457417 ---- -apiVersion: iam.cnrm.cloud.google.com/v1beta1 -kind: IAMPolicyMember -metadata: - name: container-admin -spec: - member: serviceAccount:kame-house-oidc@kame-457417.iam.gserviceaccount.com - role: roles/container.admin - resourceRef: - apiVersion: resourcemanager.cnrm.cloud.google.com/v1beta1 - kind: Project - external: kame-457417 ---- -apiVersion: iam.cnrm.cloud.google.com/v1beta1 -kind: IAMPolicyMember -metadata: - name: dns-admin -spec: - member: serviceAccount:kame-house-oidc@kame-457417.iam.gserviceaccount.com - role: roles/dns.admin - resourceRef: - apiVersion: resourcemanager.cnrm.cloud.google.com/v1beta1 - kind: Project - external: kame-457417 ---- -apiVersion: iam.cnrm.cloud.google.com/v1beta1 -kind: IAMPolicyMember -metadata: - name: workload-identity-user -spec: - member: principalSet://iam.googleapis.com/projects/316518955652/locations/global/workloadIdentityPools/github-pool/attribute.repository/ianlintner/python_dsa - role: roles/iam.workloadIdentityUser - resourceRef: - apiVersion: iam.cnrm.cloud.google.com/v1beta1 - kind: IAMServiceAccount - external: kame-house-oidc@kame-457417.iam.gserviceaccount.com ---- -apiVersion: artifactregistry.cnrm.cloud.google.com/v1beta1 -kind: ArtifactRegistryRepository -metadata: - name: python-dsa -spec: - location: us-central1 - format: DOCKER - description: Docker repository for python-dsa project diff --git a/k8s/overlays/prod/flux-image-repo.yaml b/k8s/overlays/prod/flux-image-repo.yaml deleted file mode 100644 index 8fe1b34..0000000 --- a/k8s/overlays/prod/flux-image-repo.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: image.toolkit.fluxcd.io/v1beta2 -kind: ImageRepository -metadata: - name: python-dsa - namespace: flux-system -spec: - image: us-central1-docker.pkg.dev/kame-457417/python-dsa/python-dsa - interval: 1m - secretRef: - name: artifact-registry-creds ---- -apiVersion: image.toolkit.fluxcd.io/v1beta2 -kind: ImagePolicy -metadata: - name: python-dsa-latest - namespace: flux-system -spec: - imageRepositoryRef: - name: python-dsa - policy: - semver: - range: ">=0.0.0" ---- -apiVersion: image.toolkit.fluxcd.io/v1beta2 -kind: ImageUpdateAutomation -metadata: - name: python-dsa-automation - namespace: flux-system -spec: - interval: 1m - sourceRef: - kind: GitRepository - name: flux-system - update: - strategy: Setters - path: ./k8s/overlays/prod diff --git a/k8s/overlays/prod/kustomization.yaml b/k8s/overlays/prod/kustomization.yaml deleted file mode 100644 index f2ff1aa..0000000 --- a/k8s/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: prod -resources: - - ../../base - - flux-image-repo.yaml - - artifact-registry-iam.yaml - -transformers: - - transformers/cert-namespace-patch.yaml diff --git a/k8s/overlays/prod/transformers/cert-namespace-patch.yaml b/k8s/overlays/prod/transformers/cert-namespace-patch.yaml deleted file mode 100644 index ecf2fe9..0000000 --- a/k8s/overlays/prod/transformers/cert-namespace-patch.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: builtin -kind: PatchTransformer -metadata: - name: cert-set-default-namespace -patch: |- - - op: replace - path: /metadata/namespace - value: default -target: - group: cert-manager.io - version: v1 - kind: Certificate - name: python-dsa-tls-cert