Skip to content

Commit d98ec05

Browse files
committed
feat: manage OPA bundle for EOData
1 parent 874d3a2 commit d98ec05

11 files changed

Lines changed: 294 additions & 6 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: Build and Push OPA Bundle to GHCR
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- develop
8+
paths:
9+
- 'opa/**'
10+
- '.github/workflows/build-opa-bundle.yaml'
11+
pull_request:
12+
branches:
13+
- main
14+
paths:
15+
- 'opa/**'
16+
17+
env:
18+
REGISTRY: ghcr.io
19+
IMAGE_NAME: ${{ github.repository_owner }}/opa-bundle
20+
21+
jobs:
22+
build-and-push:
23+
runs-on: ubuntu-latest
24+
permissions:
25+
contents: read
26+
packages: write
27+
steps:
28+
- name: Checkout repository
29+
uses: actions/checkout@v4
30+
31+
- name: Setup OPA
32+
run: |
33+
mkdir -p .tools
34+
cd .tools
35+
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64
36+
chmod +x opa
37+
echo "$PWD" >> $GITHUB_PATH
38+
39+
- name: Build OPA bundle
40+
run: |
41+
test -d opa/bundle
42+
opa build -b ./opa/bundle -o ./bundle.tar.gz
43+
44+
- name: Verify bundle contents
45+
run: |
46+
tar -tzf ./bundle.tar.gz | head -20
47+
echo "Bundle size: $(du -h ./bundle.tar.gz | cut -f1)"
48+
49+
- name: Set up Docker Buildx
50+
uses: docker/setup-buildx-action@v3
51+
52+
- name: Log in to Container Registry
53+
uses: docker/login-action@v3
54+
with:
55+
registry: ${{ env.REGISTRY }}
56+
username: ${{ github.actor }}
57+
password: ${{ secrets.GITHUB_TOKEN }}
58+
59+
- name: Extract metadata
60+
id: meta
61+
run: |
62+
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
63+
TAGS="$TAGS,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
64+
65+
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
66+
TAGS="$TAGS,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main"
67+
fi
68+
if [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
69+
TAGS="$TAGS,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop"
70+
fi
71+
72+
echo "tags=$TAGS" >> $GITHUB_OUTPUT
73+
74+
- name: Build and push OPA bundle image
75+
uses: docker/build-push-action@v5
76+
with:
77+
context: .
78+
file: ./opa/Dockerfile.bundle
79+
push: ${{ github.event_name != 'pull_request' }}
80+
tags: ${{ steps.meta.outputs.tags }}
81+
labels: |
82+
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
83+
org.opencontainers.image.revision=${{ github.sha }}
84+
85+
- name: Summary
86+
run: |
87+
echo "## OPA Bundle Build Summary" >> $GITHUB_STEP_SUMMARY
88+
echo "" >> $GITHUB_STEP_SUMMARY
89+
echo "**Registry**: ${{ env.REGISTRY }}" >> $GITHUB_STEP_SUMMARY
90+
echo "**Image**: ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
91+
echo "**Tags**: ${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
92+
echo "**Source**: ./opa/bundle (local repository)" >> $GITHUB_STEP_SUMMARY

argocd/eoepca/data-proxy/parts/opa-config.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ apiVersion: v1
22
data:
33
config.yaml: |-
44
services:
5-
oci-gitlab:
6-
url: https://registry.gitlab.si.c-s.fr
5+
github-opa:
6+
url: https://ghcr.io
77
type: oci
88
credentials:
99
bearer:
10-
scheme: "Basic"
11-
token: "${GITLAB_TOKEN}"
10+
scheme: "Bearer"
11+
token: "${GITHUB_TOKEN}"
1212
1313
bundles:
1414
mybundle:
15-
service: oci-gitlab
16-
resource: registry.gitlab.si.c-s.fr/dedl/data/opa-bundle:main
15+
service: github-opa
16+
resource: ghcr.io/eoepca/opa-bundle:latest
1717
polling:
1818
min_delay_seconds: 30
1919
max_delay_seconds: 60

opa/Dockerfile.bundle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM scratch
2+
COPY bundle.tar.gz /bundle.tar.gz

opa/OPA_BUNDLE_DISTRIBUTION.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# OPA Bundle Distribution with GitHub Container Registry
2+
3+
This setup builds the OPA bundle directly from this repository under `opa/bundle` and publishes it to GitHub Container Registry.
4+
5+
## Repository Layout
6+
7+
```
8+
eoepca-plus/
9+
├── opa/
10+
│ ├── bundle/
11+
│ │ ├── data.yaml
12+
│ │ └── policy/
13+
│ ├── Dockerfile.bundle
14+
│ └── OPA_BUNDLE_DISTRIBUTION.md
15+
├── .github/workflows/build-opa-bundle.yaml
16+
└── argocd/eoepca/data-proxy/parts/opa-config.yaml
17+
```
18+
19+
## CI Workflow
20+
21+
The workflow in `.github/workflows/build-opa-bundle.yaml` builds and pushes OCI image to `ghcr.io/<org>/opa-bundle`.
22+
23+
Triggers:
24+
- push on `main` and `develop` when `opa/**` changes
25+
- pull request to `main` when `opa/**` changes (build only)
26+
- manual dispatch
27+
28+
## OPA Runtime Configuration
29+
30+
`argocd/eoepca/data-proxy/parts/opa-config.yaml` should point to GHCR:
31+
32+
## Local Validation
33+
34+
```bash
35+
cd eoepca-plus
36+
opa build -b ./opa/bundle -o ./bundle.tar.gz
37+
tar -tzf ./bundle.tar.gz | head -20
38+
```
39+
40+
## Kubernetes Secret
41+
42+
```bash
43+
kubectl create secret generic opa-credentials \
44+
--from-literal=GITHUB_TOKEN=$GITHUB_TOKEN \
45+
-n data-proxy
46+
```
47+
48+
## Troubleshooting
49+
50+
1. Build fails in CI
51+
- Verify `opa/bundle` exists and contains valid Rego/YAML.
52+
- Run `opa build -b ./opa/bundle` locally.
53+
54+
2. OPA does not refresh bundle
55+
- Check sidecar logs: `kubectl logs -n data-proxy <pod> -c opa | grep -i bundle`.
56+
- Verify `GITHUB_TOKEN` is present in namespace `data-proxy`.
57+
58+
3. Image pull issues
59+
- Confirm image exists: `docker pull ghcr.io/eoepca/opa-bundle:latest`.
60+
- Confirm token has package read access.
61+
max_delay_seconds: 0
62+
```
63+
64+
### Bundle Signing (Optional)
65+
66+
For production environments, consider:
67+
- OPA bundle signing and verification
68+
- Image signing with Cosign
69+
- Policy as code for bundle contents
70+
71+
## Related Documentation
72+
73+
- [OPA Bundle Documentation](https://www.openpolicyagent.org/docs/latest/management-bundles/)
74+
- [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)
75+
- [Kubernetes Secret Management](https://kubernetes.io/docs/concepts/configuration/secret/)

opa/bundle/data.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# This file defines the authorization policies for the OPA (Open Policy Agent) integration in the DEDL HDA API.
2+
# It specifies the access control rules for different user roles and their permissions to discover, search, and download datasets.
3+
# Empty lists indicate no access for that specific action, while patterns with wildcards allow access to matching datasets.
4+
5+
authz:
6+
# Baseline = unauthenticated / anonymous access
7+
baseline:
8+
discover: ["S1_SAR_GRD"]
9+
search: ["S1_SAR_GRD"]
10+
download: ["S1_SAR_GRD"]
11+
# The following access requires roles that are assigned to users via the OIDC provider
12+
# and included in the JWT token claims
13+
roles:
14+
admin:
15+
discover: ["*"]
16+
search: ["*"]
17+
download: ["*"]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package stac.collections.authorize
2+
3+
import rego.v1
4+
5+
import data.stac.patterns.allowed
6+
7+
default allow := false
8+
9+
# Input contract expected:
10+
# input.collection_id: string
11+
# input.operation: "discover" | "search" | "download"
12+
# input.roles: [string] (already verified & extracted by the proxy; [] for anonymous)
13+
14+
allow if {
15+
cid := input.collection_id
16+
op := input.operation
17+
roles := object.get(input, "roles", [])
18+
19+
pats := allowed(op, roles)
20+
21+
some p in pats
22+
glob.match(p, ["/"], cid)
23+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package stac.collections.list
2+
3+
import rego.v1
4+
5+
import data.stac.patterns.allowed
6+
7+
default patterns := []
8+
9+
patterns := pats if {
10+
op := input.operation
11+
roles := object.get(input, "roles", [])
12+
pats := allowed(op, roles)
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package stac.cql2.collections
2+
3+
import rego.v1
4+
import data.stac.cql2.common.filter_for_property
5+
6+
# For Collection Search: filter on the Collection field "id"
7+
filter := filter_for_property("id")

opa/bundle/policy/cql2/common.rego

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package stac.cql2.common
2+
3+
import data.stac.patterns.allowed
4+
5+
filter_for_property(prop) := out if {
6+
pats := allowed(input.operation, effective_roles)
7+
out := patterns_to_or_filter(prop, pats)
8+
}
9+
10+
effective_roles := object.get(input, "roles", [])
11+
12+
patterns_to_or_filter(prop, pats) := {"op": "=", "args": [1, 0]} if count(pats) == 0
13+
14+
patterns_to_or_filter(prop, pats) := preds[0] if {
15+
preds := [pattern_to_pred(prop, pats[_])]
16+
count(preds) == 1
17+
}
18+
19+
patterns_to_or_filter(prop, pats) := {"op": "or", "args": preds} if {
20+
preds := [pattern_to_pred(prop, pats[_])]
21+
count(preds) > 1
22+
}
23+
24+
pattern_to_pred(prop, p) := {"op": "=", "args": [{"property": prop}, p]} if {
25+
not contains(p, "*")
26+
not contains(p, "?")
27+
}
28+
29+
# wildcard: '*' => LIKE
30+
pattern_to_pred(prop, p) := {"op": "like", "args": [{"property": prop}, glob_to_like(p)]} if {
31+
contains(p, "*")
32+
}
33+
34+
# wildcard: '?' => LIKE
35+
pattern_to_pred(prop, p) := {"op": "like", "args": [{"property": prop}, glob_to_like(p)]} if {
36+
contains(p, "?")
37+
}
38+
39+
glob_to_like(p) := replace(replace(p, "*", "%"), "?", "_")

opa/bundle/policy/cql2/items.rego

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package stac.cql2.items
2+
3+
import rego.v1
4+
import data.stac.cql2.common.filter_for_property
5+
6+
# For Item Search: filter on the STAC Item field "collection"
7+
filter := filter_for_property("collection")

0 commit comments

Comments
 (0)