registry-add-entry

This change adds a registry-add-entry action.

Signed-off-by: Ben Hale <bhale@vmware.com>
This commit is contained in:
Ben Hale 2020-11-23 19:14:44 -08:00
parent f9e5484fd3
commit b3d523fb36
No known key found for this signature in database
GPG Key ID: A49396AF5C094C40
18 changed files with 580 additions and 17 deletions

View File

@ -3,11 +3,14 @@ name: Action buildpack-compute-metadata
pull_request:
paths:
- buildpack/compute-metadata/**
- internal/**
push:
branches:
- main
- test
paths:
- buildpack/compute-metadata/**
- internal/**
release:
types:
- published

View File

@ -3,11 +3,14 @@ name: Action buildpackage-verify-metadata
pull_request:
paths:
- buildpackage/verify-metadata/**
- internal/**
push:
branches:
- main
- test
paths:
- buildpackage/verify-metadata/**
- internal/**
release:
types:
- published

View File

@ -0,0 +1,78 @@
name: Action registry-add-entry
"on":
pull_request:
paths:
- internal/**
- registry/add-entry/**
- registry/internal/**
push:
branches:
- main
- test
paths:
- internal/**
- registry/add-entry/**
- registry/internal/**
release:
types:
- published
jobs:
create-action:
name: Create Action
runs-on:
- ubuntu-latest
steps:
- if: ${{ github.event_name != 'pull_request' || ! github.event.pull_request.head.repo.fork }}
name: Docker login ghcr.io
uses: docker/login-action@v1
with:
password: ${{ secrets.IMPLEMENTATION_GITHUB_TOKEN }}
registry: ghcr.io
username: ${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }}
- uses: actions/checkout@v2
- id: version
name: Compute Version
run: |
#!/usr/bin/env bash
set -euo pipefail
if [[ ${GITHUB_REF} =~ refs/tags/v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
elif [[ ${GITHUB_REF} =~ refs/heads/(.+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
VERSION=$(git rev-parse --short HEAD)
fi
echo "::set-output name=version::${VERSION}"
echo "Selected ${VERSION} from
* ref: ${GITHUB_REF}
* sha: ${GITHUB_SHA}
"
- name: Create Action
run: |
#!/usr/bin/env bash
set -euo pipefail
echo "::group::Building ${TARGET}:${VERSION}"
docker build \
--file Dockerfile \
--build-arg "SOURCE=${SOURCE}" \
--tag "${TARGET}:${VERSION}" \
.
echo "::endgroup::"
if [[ "${PUSH}" == "true" ]]; then
echo "::group::Pushing ${TARGET}:${VERSION}"
docker push "${TARGET}:${VERSION}"
echo "::endgroup::"
else
echo "Skipping push"
fi
env:
PUSH: ${{ github.event_name != 'pull_request' }}
SOURCE: registry/add-entry/cmd
TARGET: ghcr.io/buildpacks/actions/registry/add-entry
VERSION: ${{ steps.version.outputs.version }}

View File

@ -2,14 +2,17 @@ name: Action registry-compute-metadata
"on":
pull_request:
paths:
- registry/*
- internal/**
- registry/compute-metadata/**
- registry/internal/**
push:
branches:
- main
- test
paths:
- registry/*
- internal/**
- registry/compute-metadata/**
- registry/internal/**
release:
types:
- published

View File

@ -2,13 +2,16 @@ name: Action registry-request-add-entry
"on":
pull_request:
paths:
- registry/*
- internal/**
- registry/internal/**
- registry/request-add-entry/**
push:
branches:
- main
- test
paths:
- registry/*
- internal/**
- registry/internal/**
- registry/request-add-entry/**
release:
types:

View File

@ -2,13 +2,16 @@ name: Action registry-request-yank-entry
"on":
pull_request:
paths:
- registry/*
- internal/**
- registry/internal/**
- registry/request-yank-entry/**
push:
branches:
- main
- test
paths:
- registry/*
- internal/**
- registry/internal/**
- registry/request-yank-entry/**
release:
types:

View File

@ -2,13 +2,16 @@ name: Action registry-verify-namespace-owner
"on":
pull_request:
paths:
- registry/*
- internal/**
- registry/internal/**
- registry/verify-namespace-owner/**
push:
branches:
- main
- test
paths:
- registry/*
- internal/**
- registry/internal/**
- registry/verify-namespace-owner/**
release:
types:

View File

@ -9,6 +9,7 @@
- [Buildpackage](#buildpackage)
- [Verify Metadata Action](#verify-metadata-action)
- [Registry](#registry)
- [Add Entry Action](#add-entry-action)
- [Compute Metadata Action](#compute-metadata-action)
- [Request Add Entry Action](#request-add-entry-action)
- [Request Yank Entry Action](#request-yank-entry-action)
@ -61,6 +62,32 @@ with:
## Registry
[bri]: https://github.com/buildpacks/registry-index
### Add Entry Action
The `registry/add-entry` adds an entry to the [Buildpack Registry Index][bri].
```yaml
uses: docker://ghcr.io/buildpacks/actions/registry/add-entry
with:
token: ${{ secrets.BOT_TOKEN }}
owner: ${{ env.INDEX_OWNER }}
repository: ${{ env.INDEX_REPOSITORY }}
namespace: ${{ steps.metadata.outputs.namespace }}
name: ${{ steps.metadata.outputs.name }}
version: ${{ steps.metadata.outputs.version }}
address: ${{ steps.metadata.outputs.address }}
```
#### Inputs <!-- omit in toc -->
| Parameter | Description
| :-------- | :----------
| `token` | A GitHub token with permissions to commit to the registry index repository.
| `owner` | The owner name of the registry index repository.
| `repository` | The repository name of the registry index repository.
| `namespace` | The namespace of the buildpack to register.
| `name` | The name of the buildpack to register.
| `version` | The version of the buildpack to register.
| `address` | The address of the buildpack to register.
### Compute Metadata Action
The `registry/compute-metadata` action parses a [`buildpacks/registry-index`][bri] issue and exposes the contents as output parameters.

View File

@ -0,0 +1,154 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package entry
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/google/go-github/v32/github"
"github.com/buildpacks/github-actions/internal/toolkit"
"github.com/buildpacks/github-actions/registry/internal/index"
"github.com/buildpacks/github-actions/registry/internal/services"
)
func AddEntry(tk toolkit.Toolkit, repositories services.RepositoriesService) error {
owner, ok := tk.GetInput("owner")
if !ok {
return toolkit.FailedError("owner must be set")
}
repository, ok := tk.GetInput("repository")
if !ok {
return toolkit.FailedError("repository must be set")
}
namespace, ok := tk.GetInput("namespace")
if !ok {
return toolkit.FailedError("namespace must be set")
}
name, ok := tk.GetInput("name")
if !ok {
return toolkit.FailedError("name must be set")
}
version, ok := tk.GetInput("version")
if !ok {
return toolkit.FailedError("version must be set")
}
address, ok := tk.GetInput("address")
if !ok {
return toolkit.FailedError("address must be set")
}
file := index.Path(namespace, name)
content, _, _, err := repositories.GetContents(context.Background(), owner, repository, file, nil)
if err2, ok := err.(*github.ErrorResponse); ok && err2.Response.StatusCode == http.StatusNotFound {
fmt.Printf("New Index: %s\n", name)
content = &github.RepositoryContent{}
} else if err != nil {
return toolkit.FailedErrorf("unable to read index %s\n%w", name, err)
}
s, err := content.GetContent()
if err != nil {
return toolkit.FailedErrorf("unable to get index content\n%w", err)
}
entries, err := unmarshalEntries(s)
if err != nil {
return toolkit.FailedErrorf("unable to unmarshal entries\n%w", err)
}
if contains(entries, namespace, version) {
return toolkit.FailedErrorf("index %s already has namespace %s and version %s", name, namespace, version)
}
entries = append(entries, index.Entry{
Namespace: namespace,
Name: name,
Version: version,
Address: address,
})
s, err = marshalEntries(entries)
if err != nil {
return toolkit.FailedErrorf("unable to marshal entries\n%w", err)
}
if _, _, err := repositories.CreateFile(context.Background(), owner, repository, file, &github.RepositoryContentFileOptions{
Author: &github.CommitAuthor{
Name: github.String("buildpacks-bot"),
Email: github.String("cncf-buildpacks-maintainers@lists.cncf.io"),
},
Message: github.String(fmt.Sprintf("ADD %s/%s@%s", namespace, name, version)),
SHA: content.SHA,
Content: []byte(s),
}); err != nil {
return err
}
fmt.Printf("Added %s/%s@%s\nx", namespace, name, version)
return nil
}
func contains(entries []index.Entry, namespace string, version string) bool {
for _, e := range entries {
if e.Namespace == namespace && e.Version == version {
return true
}
}
return false
}
func marshalEntries(entries []index.Entry) (string, error) {
b := &bytes.Buffer{}
j := json.NewEncoder(b)
for _, e := range entries {
if err := j.Encode(e); err != nil {
return "", err
}
}
return b.String(), nil
}
func unmarshalEntries(content string) ([]index.Entry, error) {
var entries []index.Entry
scanner := bufio.NewScanner(strings.NewReader(content))
for scanner.Scan() {
var e index.Entry
if err := json.Unmarshal(scanner.Bytes(), &e); err != nil {
return nil, err
}
entries = append(entries, e)
}
return entries, scanner.Err()
}

View File

@ -0,0 +1,151 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package entry_test
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"testing"
"github.com/google/go-github/v32/github"
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
"github.com/stretchr/testify/mock"
"github.com/buildpacks/github-actions/internal/toolkit"
entry "github.com/buildpacks/github-actions/registry/add-entry"
"github.com/buildpacks/github-actions/registry/internal/index"
"github.com/buildpacks/github-actions/registry/internal/services"
)
func TestAddEntry(t *testing.T) {
spec.Run(t, "add-entry", func(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
ExpectWithOffset = NewWithT(t).ExpectWithOffset
r = &services.MockRepositoriesService{}
rOpts *github.RepositoryContentGetOptions
tk = &toolkit.MockToolkit{}
)
asJSONString := func(v interface{}) string {
b, err := json.Marshal(v)
ExpectWithOffset(1, err).NotTo(HaveOccurred())
return string(b)
}
it.Before(func() {
tk.On("GetInput", "owner").Return("test-owner", true)
tk.On("GetInput", "repository").Return("test-repository", true)
tk.On("GetInput", "namespace").Return("test-namespace", true)
tk.On("GetInput", "name").Return("test-name", true)
tk.On("GetInput", "version").Return("test-version", true)
tk.On("GetInput", "address").Return("test-address", true)
})
context("index does not exist", func() {
it.Before(func() {
r.On("GetContents", mock.Anything, "test-owner", "test-repository", filepath.Join("te", "st", "test-namespace_test-name"), rOpts).
Return(nil, nil, nil, &github.ErrorResponse{Response: &http.Response{StatusCode: http.StatusNotFound}})
})
it("creates new index", func() {
r.On("CreateFile", mock.Anything, "test-owner", "test-repository", filepath.Join("te", "st", "test-namespace_test-name"), &github.RepositoryContentFileOptions{
Author: &github.CommitAuthor{
Name: github.String("buildpacks-bot"),
Email: github.String("cncf-buildpacks-maintainers@lists.cncf.io"),
},
Message: github.String("ADD test-namespace/test-name@test-version"),
Content: []byte(fmt.Sprintf("%s\n", asJSONString(index.Entry{
Namespace: "test-namespace",
Name: "test-name",
Version: "test-version",
Address: "test-address",
}))),
}).
Return(nil, nil, nil)
Expect(entry.AddEntry(tk, r)).To(Succeed())
})
})
context("index does exist", func() {
it("fails if version already exists", func() {
r.On("GetContents", mock.Anything, "test-owner", "test-repository", filepath.Join("te", "st", "test-namespace_test-name"), rOpts).
Return(&github.RepositoryContent{
Content: github.String(asJSONString(index.Entry{
Namespace: "test-namespace",
Name: "test-name",
Version: "test-version",
Address: "test-address",
})),
SHA: github.String("test-sha"),
}, nil, nil, nil)
Expect(entry.AddEntry(tk, r)).
To(MatchError("::error ::index test-name already has namespace test-namespace and version test-version"))
})
it("adds entry to index", func() {
r.On("GetContents", mock.Anything, "test-owner", "test-repository", filepath.Join("te", "st", "test-namespace_test-name"), rOpts).
Return(&github.RepositoryContent{
Content: github.String(asJSONString(index.Entry{
Namespace: "another-namespace",
Name: "test-name",
Version: "test-version",
Address: "test-address",
})),
SHA: github.String("test-sha"),
}, nil, nil, nil)
r.On("CreateFile", mock.Anything, "test-owner", "test-repository", filepath.Join("te", "st", "test-namespace_test-name"), &github.RepositoryContentFileOptions{
Author: &github.CommitAuthor{
Name: github.String("buildpacks-bot"),
Email: github.String("cncf-buildpacks-maintainers@lists.cncf.io"),
},
Message: github.String("ADD test-namespace/test-name@test-version"),
Content: []byte(fmt.Sprintf("%s\n%s\n",
asJSONString(index.Entry{
Namespace: "another-namespace",
Name: "test-name",
Version: "test-version",
Address: "test-address",
}),
asJSONString(index.Entry{
Namespace: "test-namespace",
Name: "test-name",
Version: "test-version",
Address: "test-address",
}),
)),
SHA: github.String("test-sha"),
}).
Return(nil, nil, nil)
Expect(entry.AddEntry(tk, r)).To(Succeed())
})
})
}, spec.Report(report.Terminal{}))
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"context"
"fmt"
"os"
"github.com/google/go-github/v32/github"
"golang.org/x/oauth2"
"github.com/buildpacks/github-actions/internal/toolkit"
entry "github.com/buildpacks/github-actions/registry/add-entry"
)
func main() {
tk := &toolkit.DefaultToolkit{}
t, ok := tk.GetInput("token")
if !ok {
fmt.Println(toolkit.FailedError("token must be specified"))
os.Exit(1)
}
gh := github.NewClient(oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{AccessToken: t})))
if err := entry.AddEntry(tk, gh.Repositories); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@ -16,10 +16,10 @@
package index
type Index struct {
type Entry struct {
Namespace string `json:"ns"`
Name string
Version string
Yanked bool
Name string `json:"name"`
Version string `json:"version"`
Yanked bool `json:"yanked"`
Address string `json:"addr"`
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package index
import (
"fmt"
"path/filepath"
)
func Path(namespace string, name string) string {
var path string
switch len(name) {
case 1:
path = "1"
case 2:
path = "2"
case 3:
path = filepath.Join("3", name[0:2])
default:
path = filepath.Join(name[0:2], name[2:4])
}
return filepath.Join(path, filepath.Join(fmt.Sprintf("%s_%s", namespace, name)))
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package index_test
import (
"path/filepath"
"testing"
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/github-actions/registry/internal/index"
)
func TestPath(t *testing.T) {
spec.Run(t, "path", func(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
)
it("returns path for 1 character name", func() {
Expect(index.Path("test-namespace", "a")).To(Equal(filepath.Join("1", "test-namespace_a")))
})
it("returns path for 2 character name", func() {
Expect(index.Path("test-namespace", "ab")).To(Equal(filepath.Join("2", "test-namespace_ab")))
})
it("returns path for 3 character name", func() {
Expect(index.Path("test-namespace", "abc")).To(Equal(filepath.Join("3", "ab", "test-namespace_abc")))
})
it("returns path for 4+ character name", func() {
Expect(index.Path("test-namespace", "abcd")).To(Equal(filepath.Join("ab", "cd", "test-namespace_abcd")))
})
})
}

View File

@ -39,7 +39,7 @@ var restrictedNamespaces = []string{
}
type Namespace struct {
Owners []Owner
Owners []Owner `json:"owners"`
}
func IsRestricted(namespace string) bool {

View File

@ -22,8 +22,8 @@ const (
)
type Owner struct {
ID int64
Type string
ID int64 `json:"id"`
Type string `json:"type"`
}
type OwnerPredicate func(Owner) bool

View File

@ -75,7 +75,7 @@ func VerifyNamespaceOwner(tk toolkit.Toolkit, organizations services.Organizatio
s, err := content.GetContent()
if err != nil {
return toolkit.FailedError("unable to get namespace content")
return toolkit.FailedErrorf("unable to get namespace content\n%w", err)
}
var n namespace.Namespace

View File

@ -63,7 +63,6 @@ func TestVerifyNamespaceOwner(t *testing.T) {
context("unknown namespace", func() {
it.Before(func() {
r.On("GetContents", mock.Anything, "test-owner", "test-repository", filepath.Join("v1", "test-namespace.json"), rOpts).
Return(nil, nil, nil, &github.ErrorResponse{Response: &http.Response{StatusCode: http.StatusNotFound}})
})