diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..eead852 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,159 @@ +name: Build docker images +on: + push: +jobs: + matrix: + name: Compute build matrix from pypi API + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: './build-matrix/go.mod' + + - name: Run matrix generator tests + working-directory: build-matrix + run: go test ./ + + - id: matrix + working-directory: build-matrix + run: | + MATRIX=$(go run ./) + echo ${MATRIX} | jq + echo 'matrix=[{"ansible":"10.2","additional_tags":["10"]},{"ansible":"10.1","additional_tags":[]}]' >> $GITHUB_OUTPUT + #echo "matrix=${MATRIX}" >> $GITHUB_OUTPUT + build: + needs: [ matrix ] + runs-on: ubuntu-latest + name: Build ansible ${{ matrix.versions.ansible }} + permissions: + packages: write + contents: read + strategy: + matrix: + versions: ${{ fromJson(needs.matrix.outputs.matrix) }} + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + # This is not required but recommended using it to be able to build multi-platform images, export cache, etc. + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Allows to build arm64 images + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Log in to Github Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Docker images + uses: docker/build-push-action@v6 + with: + push: true + build-args: | + ANSIBLE_VERSION=${{ matrix.versions.ansible }} + platforms: | + linux/amd64 + linux/arm64 + tags: | + ghcr.io/${{ github.repository }}:${{ matrix.versions.ansible }}-${{ github.sha }} + + test: + needs: [ matrix, build ] + runs-on: ubuntu-latest + strategy: + matrix: + versions: ${{ fromJson(needs.matrix.outputs.matrix) }} + steps: + - name: Test ansible version + run: + docker run ghcr.io/${{ github.repository }}:${{ matrix.versions.ansible }}-${{ github.sha }} ansible-community --version | grep 'Ansible community version ${{ matrix.versions.ansible }}' + + deploy: + needs: [ matrix, test ] + runs-on: ubuntu-latest + strategy: + matrix: + versions: ${{ fromJson(needs.matrix.outputs.matrix) }} + if: ${{ github.ref == 'refs/heads/main' }} + permissions: + packages: write + contents: read + steps: + - name: Login to Amazon ECR + run: aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${REPOSITORY_PATH} + env: + REPOSITORY_PATH: ${{ secrets.PUBLIC_RUNNER_ANSIBLE_ECR_REPOSITORY_URL }} + + - name: Log in to Github Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Tag images + run: | + echo "Tagging image to ghcr.io/${{ github.repository }}:${{ matrix.versions.ansible }}" + docker tag ghcr.io/${{ github.repository }}:${{ matrix.versions.ansible }}-${{ github.sha }} ghcr.io/${{ github.repository }}:${{ matrix.versions.ansible }} + for tag in ${{ join(fromJSON(matrix.versions.additional_tags), ' ') }} + do + echo "Tagging image to $tag" + docker tag ghcr.io/${{ github.repository }}:${tag} + done + # TODO Tag ECR + + - name: Push images + run: | + echo "Pushing image ghcr.io/${{ github.repository }}:${{ matrix.versions.ansible }}" + docker push ghcr.io/${{ github.repository }}:${{ matrix.versions.ansible }} + for tag in ${{ join(fromJSON(matrix.versions.additional_tags), ' ') }} + do + echo "Pushing image $tag" + docker push ghcr.io/${{ github.repository }}:${tag} + done + # TODO Push to ECR + + cleanup: + name: Cleanup unused docker images + needs: [ matrix, test ] + runs-on: ubuntu-latest + strategy: + matrix: + versions: ${{ fromJson(needs.matrix.outputs.matrix) }} + if: ${{ github.ref != 'refs/heads/main' }} + steps: + - name: Install regctl + uses: regclient/actions/regctl-installer@main + with: + release: 'v0.7.0' + - name: Remove image + run: regctl manifest delete --force-tag-dereference ghcr.io/${{ github.repository }}:${{ matrix.versions.ansible }}-${{ github.sha }} + + cleanup-prod: + name: Cleanup unused prod images + needs: [ matrix, deploy ] + runs-on: ubuntu-latest + strategy: + matrix: + versions: ${{ fromJson(needs.matrix.outputs.matrix) }} + if: ${{ github.ref == 'refs/heads/main' }} + steps: + - name: Install regctl + uses: regclient/actions/regctl-installer@main + with: + release: 'v0.7.0' + - name: Remove image + run: regctl manifest delete --force-tag-dereference ghcr.io/${{ github.repository }}:${{ matrix.versions.ansible }}-${{ github.sha }} + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..485aeac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM python:3.12 +ARG ANSIBLE_VERSION=10.0 +RUN pip install ansible==${ANSIBLE_VERSION}.* ansible-runner~=2.4 diff --git a/build-matrix/go.mod b/build-matrix/go.mod new file mode 100644 index 0000000..5285690 --- /dev/null +++ b/build-matrix/go.mod @@ -0,0 +1,11 @@ +module github.com/spacelift-io/build-matrix + +go 1.22.1 + +require ( + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/build-matrix/go.sum b/build-matrix/go.sum new file mode 100644 index 0000000..79eafcc --- /dev/null +++ b/build-matrix/go.sum @@ -0,0 +1,11 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/build-matrix/main.go b/build-matrix/main.go new file mode 100755 index 0000000..bd908e4 --- /dev/null +++ b/build-matrix/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sort" + + "github.com/Masterminds/semver/v3" +) + +const ( + // Define the oldest major version we care about, we do not want to build image starting ansible 1.0 + minSupportedMajor = 7 +) + +type ReleaseResponse struct { + Releases map[string]any `json:"releases"` +} + +type matrixVersion struct { + Ansible string `json:"ansible"` + AdditionalTags []string `json:"additional_tags"` +} +type Matrix []matrixVersion + +func main() { + resp, err := http.Get("https://pypi.org/pypi/ansible/json") + if err != nil { + log.Fatal(err) + } + + matrixOutput := GenerateBuildMatrix(resp.Body, minSupportedMajor) + + output, err := json.Marshal(matrixOutput) + if err != nil { + log.Fatal(err) + } + fmt.Print(string(output)) +} + +func GenerateBuildMatrix(reader io.Reader, minSupportedMajor uint64) Matrix { + releases := ReleaseResponse{} + if err := json.NewDecoder(reader).Decode(&releases); err != nil { + log.Fatal(err) + } + + var versions []*semver.Version + + for v := range releases.Releases { + version, err := semver.NewVersion(v) + if err != nil { + log.Printf("Unable to parse version %s\n", v) + continue + } + versions = append(versions, version) + } + + sort.Slice(versions, func(i, j int) bool { + return versions[j].LessThan(versions[i]) + }) + + versionGroupedByMajor := make(map[uint64][]*semver.Version) + + for _, version := range versions { + if version.Major() < minSupportedMajor { + break + } + versionGroupedByMajor[version.Major()] = append(versionGroupedByMajor[version.Major()], version) + } + + minorVersionDeduplication := map[string]any{} + matrix := Matrix{} + for _, v := range versionGroupedByMajor { + for i, version := range v { + key := fmt.Sprintf("%d.%d", version.Major(), version.Minor()) + if _, exists := minorVersionDeduplication[key]; exists { + continue + } + additionalTags := make([]string, 0) + if i == 0 { + additionalTags = append(additionalTags, fmt.Sprintf("%d", version.Major())) + } + minorVersionDeduplication[key] = struct{}{} + matrix = append(matrix, matrixVersion{ + Ansible: key, + AdditionalTags: additionalTags, + }) + } + } + + return matrix +} diff --git a/build-matrix/main_test.go b/build-matrix/main_test.go new file mode 100644 index 0000000..0398763 --- /dev/null +++ b/build-matrix/main_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateBuildMatrix(t *testing.T) { + fakePythonVersions := ReleaseResponse{ + Releases: map[string]any{ + "1.1.0": struct{}{}, + "1.1.1": struct{}{}, + "2.10.0": struct{}{}, + "2.11.0": struct{}{}, + "2.11.2": struct{}{}, + "3.1.0": struct{}{}, + "3.1.1": struct{}{}, + "3.2.0": struct{}{}, + }, + } + + fakeJsonResponse, err := json.Marshal(fakePythonVersions) + require.NoError(t, err) + + matrix := GenerateBuildMatrix(bytes.NewReader(fakeJsonResponse), 2) + expectedMatrix := Matrix{ + { + Ansible: "3.2", + AdditionalTags: []string{"3"}, + }, + { + Ansible: "3.1", + AdditionalTags: []string{}, + }, + { + Ansible: "2.11", + AdditionalTags: []string{"2"}, + }, + { + Ansible: "2.10", + AdditionalTags: []string{}, + }, + } + assert.Equal(t, expectedMatrix, matrix) +}