diff --git a/cmd/otk-resolve-ostree-commit/export_test.go b/cmd/otk-resolve-ostree-commit/export_test.go new file mode 100644 index 000000000..77c5f1a93 --- /dev/null +++ b/cmd/otk-resolve-ostree-commit/export_test.go @@ -0,0 +1,5 @@ +package main + +var ( + Run = run +) diff --git a/cmd/otk-resolve-ostree-commit/main.go b/cmd/otk-resolve-ostree-commit/main.go new file mode 100644 index 000000000..e874443a3 --- /dev/null +++ b/cmd/otk-resolve-ostree-commit/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/osbuild/images/pkg/ostree" +) + +// All otk external inputs are nested under a top-level "tree" +type Tree struct { + Tree Input `json:"tree"` +} + +// Input represents the user-provided inputs that will be used to resolve an +// ostree commit ID. +type Input struct { + // URL of the repo where the commit can be fetched. + URL string `json:"url"` + + // Ref to resolve. + Ref string `json:"ref"` + + // Whether to use RHSM secrets when resolving and fetching the commit. + RHSM bool `json:"rhsm,omitempty"` +} + +// Output contains everything needed to write a manifest that requires pulling +// an ostree commit. +type Output struct { + Const OutputConst `json:"const"` +} + +type OutputConst struct { + // Ref of the commit (can be empty). + Ref string `json:"ref,omitempty"` + + // URL of the repo where the commit can be fetched. + URL string `json:"url"` + + // Secrets type to use when pulling the ostree commit content + // (e.g. org.osbuild.rhsm.consumer). + Secrets string `json:"secrets,omitempty"` + + // Checksum of the commit. + Checksum string `json:"checksum"` +} + +func run(r io.Reader, w io.Writer) error { + var inputTree Tree + if err := json.NewDecoder(r).Decode(&inputTree); err != nil { + return err + } + + sourceSpec := ostree.SourceSpec(inputTree.Tree) + commitSpec, err := ostree.Resolve(sourceSpec) + if err != nil { + return fmt.Errorf("failed to resolve ostree commit: %w", err) + } + + output := map[string]Output{ + "tree": { + Const: OutputConst{ + Ref: commitSpec.Ref, + URL: commitSpec.URL, + Secrets: commitSpec.Secrets, + Checksum: commitSpec.Checksum, + }, + }, + } + outputJson, err := json.MarshalIndent(output, "", " ") + if err != nil { + return fmt.Errorf("cannot marshal response: %w", err) + } + fmt.Fprintf(w, "%s\n", outputJson) + return nil +} + +func main() { + if err := run(os.Stdin, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err.Error()) + os.Exit(1) + } +} diff --git a/cmd/otk-resolve-ostree-commit/main_test.go b/cmd/otk-resolve-ostree-commit/main_test.go new file mode 100644 index 000000000..841ff5622 --- /dev/null +++ b/cmd/otk-resolve-ostree-commit/main_test.go @@ -0,0 +1,193 @@ +package main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + resolver "github.com/osbuild/images/cmd/otk-resolve-ostree-commit" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var commitMap = map[string]string{ + "centos/9/x86_64/edge": "d04105393ca0617856b34f897842833d785522e41617e17dca2063bf97e294ef", + "fake/ref/f": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "fake/ref/9": "9999999999999999999999999999999999999999999999999999999999999999", + "test/ref/alpha": "9b1ea9a8e10dc27d4ea40545bec028ad8e360dd26d18de64b0f6217833a8443d", + "test/ref/one": "7433e1b49fb136d61dcca49ebe34e713fdbb8e29bf328fe90819628f71b86105", +} + +// Create a test server that responds with the commit ID that corresponds to +// the ref. +func createTestServer(refIDs map[string]string) *httptest.Server { + handler := http.NewServeMux() + handler.HandleFunc("/refs/heads/", func(w http.ResponseWriter, r *http.Request) { + reqRef := strings.TrimPrefix(r.URL.Path, "/refs/heads/") + id, ok := refIDs[reqRef] + if !ok { + http.NotFound(w, r) + return + } + fmt.Fprint(w, id) + }) + + return httptest.NewServer(handler) +} + +func TestResolver(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + repoServer := createTestServer(commitMap) + defer repoServer.Close() + + url := repoServer.URL + for ref, id := range commitMap { + inputReq, err := json.Marshal(map[string]map[string]string{ + "tree": { + "url": url, + "ref": ref, + }, + }) + require.NoError(err) + + inpBuf := bytes.NewBuffer(inputReq) + outBuf := &bytes.Buffer{} + + assert.NoError(resolver.Run(inpBuf, outBuf)) + + var output map[string]map[string]map[string]string + require.NoError(json.Unmarshal(outBuf.Bytes(), &output)) + + expOutput := map[string]map[string]map[string]string{ + "tree": { + "const": { + "url": url, + "ref": ref, + "checksum": id, + }, + }, + } + assert.Equal(expOutput, output) + } +} + +func TestResolverByID(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + for _, id := range commitMap { + inputReq, err := json.Marshal(map[string]map[string]string{ + "tree": { + "ref": id, + }, + }) + require.NoError(err) + + inpBuf := bytes.NewBuffer(inputReq) + outBuf := &bytes.Buffer{} + + assert.NoError(resolver.Run(inpBuf, outBuf)) + + var output map[string]map[string]map[string]string + require.NoError(json.Unmarshal(outBuf.Bytes(), &output)) + + expOutput := map[string]map[string]map[string]string{ + "tree": { + "const": { + "ref": id, + "checksum": id, + "url": "", + }, + }, + } + assert.Equal(expOutput, output) + } +} +func TestResolverIDwithURL(t *testing.T) { + + require := require.New(t) + assert := assert.New(t) + + // the URL is not used when the ref is a commit ID, but it should be returned in the output + url := "https://doesnt-matter.example.org" + for _, id := range commitMap { + inputReq, err := json.Marshal(map[string]map[string]string{ + "tree": { + "ref": id, + "url": url, + }, + }) + require.NoError(err) + + inpBuf := bytes.NewBuffer(inputReq) + outBuf := &bytes.Buffer{} + + assert.NoError(resolver.Run(inpBuf, outBuf)) + + var output map[string]map[string]map[string]string + require.NoError(json.Unmarshal(outBuf.Bytes(), &output)) + + expOutput := map[string]map[string]map[string]string{ + "tree": { + "const": { + "ref": id, + "checksum": id, + "url": url, + }, + }, + } + assert.Equal(expOutput, output) + } +} + +func TestResolverErrors(t *testing.T) { + + repoServer := createTestServer(commitMap) + defer repoServer.Close() + + type testCase struct { + url string + ref string + errSubstring string + } + + testCases := map[string]testCase{ + "bad-ref": { + url: "doesn't matter", + ref: "---", + errSubstring: "Invalid ostree ref or commit", + }, + "ref-not-found": { + url: repoServer.URL, + ref: "good/ref/but/does-not-exist", + errSubstring: "returned status: 404 Not Found", + }, + } + + for name := range testCases { + tc := testCases[name] + t.Run(name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + inputReq, err := json.Marshal(map[string]map[string]string{ + "tree": { + "url": tc.url, + "ref": tc.ref, + }, + }) + require.NoError(err) + + inpBuf := bytes.NewBuffer(inputReq) + outBuf := &bytes.Buffer{} + + assert.ErrorContains(resolver.Run(inpBuf, outBuf), tc.errSubstring) + }) + } +} diff --git a/pkg/ostree/ostree.go b/pkg/ostree/ostree.go index f7f50bb1c..a6ebe155c 100644 --- a/pkg/ostree/ostree.go +++ b/pkg/ostree/ostree.go @@ -152,7 +152,10 @@ func ResolveRef(location, ref string, consumerCerts bool, subs *rhsm.Subscriptio if consumerCerts { if subs == nil { subs, err = rhsm.LoadSystemSubscriptions() - if subs.Consumer == nil || err != nil { + if err != nil { + return "", NewResolveRefError("error adding rhsm certificates when resolving ref: %s", err) + } + if subs.Consumer == nil { return "", NewResolveRefError("error adding rhsm certificates when resolving ref") } }