Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(push): adds build and push functionality into different subcommands #21

Merged
merged 11 commits into from
Jun 7, 2022
Merged
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,43 @@ embedded in UOR artifacts.

To learn more about Universal Runtime visit the UOR Framework website at https://uor-framework.github.io.

## Development

### Requirements

- `go` version 1.17+

### Build

```
make
./bin/client -h
```
### Test

#### Unit:
```
make test-unit
```

## Basic Usage

### User Workflow

1. Create a directory with artifacts to publish to a registry as an OCI artifact. If the files reference each other, the client will replace the in-content linked files with the content address.
> WARNING: Currently, only JSON is supported for link replacement.
2. Use the `client build` command to create the output workspace with the rendered content. If the files in the workspace do not contain links to each other, skip this step.
3. Use the `client push` command to publish the workspace to a registry as an OCI artifact.
### Template content in a directory without pushing
```
# The default workspace is "client-workspace" in the current working directory
client build <directory> --output my-workspace
client build my-directory --output my-workspace
```

### Template content in a directory and push to a registry location
`client build <directory> --push --destination localhost:5000/myartifacts:latest`
### Push workspace to a registry location
```
client push my-workspace localhost:5000/myartifacts:latest
```



4 changes: 2 additions & 2 deletions builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ type Builder struct {
Source workspace.Workspace
}

// NewBuilder creates an new Builder from the source
// workspace
// NewBuilder creates a new Builder from the source
// workspace.
func NewBuilder(source workspace.Workspace) Builder {
return Builder{source}
}
Expand Down
4 changes: 2 additions & 2 deletions builder/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ func (g *Graph) AddEdge(origin, destination string) error {
}

// Root calculates to root node of the graph.
// This is calculated base on existing child nodes.
// This expected only of root node to be found.
// This is calculated based on existing child nodes.
// This expects only one root node to be found.
func (g *Graph) Root() (*Node, error) {
// FIXME(jpowe432): Optimize or redesign the chain

Expand Down
102 changes: 13 additions & 89 deletions cli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,32 @@ import (
"github.com/uor-framework/client/builder"
"github.com/uor-framework/client/builder/graph"
"github.com/uor-framework/client/builder/parser"
"github.com/uor-framework/client/registryclient/orasclient"
"github.com/uor-framework/client/util/workspace"
)

type BuildOptions struct {
*RootOptions
Destination string
RootDir string
Insecure bool
PlainHTTP bool
Configs []string
Output string
Push bool
RootDir string
Output string
}

var clientBuildExamples = templates.Examples(
`
# Template content in a directory without pushing
client build <directory>
# Template content in a directory
# The default workspace is "client-workspace" in the current working directory.
client build my-directory

# Template content in a directory and push to a registry location
client build <directory> --push --destination localhost:5000/myartifacts:latest
# Template content into a specified output directory.
client build my-directory --output my-workspace
`,
)

func NewBuildCmd(rootOpts *RootOptions) *cobra.Command {
o := BuildOptions{RootOptions: rootOpts}

cmd := &cobra.Command{
Use: "build directory",
Short: "Template, build, and publish OCI content from a local directory",
Use: "build SRC",
Short: "Template and build files from a local directory into a UOR dataset",
Example: clientBuildExamples,
SilenceErrors: false,
SilenceUsage: false,
Expand All @@ -55,12 +50,7 @@ func NewBuildCmd(rootOpts *RootOptions) *cobra.Command {
},
}

cmd.Flags().StringArrayVarP(&o.Configs, "configs", "c", o.Configs, "auth config paths")
cmd.Flags().BoolVarP(&o.Insecure, "insecure", "", o.Insecure, "allow connections to SSL registry without certs")
cmd.Flags().BoolVarP(&o.PlainHTTP, "plain-http", "", o.PlainHTTP, "use plain http and not https")
cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, "location to stored templated files")
cmd.Flags().BoolVarP(&o.Push, "push", "p", o.Push, "push workspace artifacts to registry")
cmd.Flags().StringVarP(&o.Destination, "destination", "d", o.Destination, "image location to store artifacts in a registry")

return cmd
}
Expand All @@ -80,18 +70,11 @@ func (o *BuildOptions) Validate() error {
if _, err := os.Stat(o.RootDir); err != nil {
return fmt.Errorf("workspace directory %q: %v", o.RootDir, err)
}

if o.Push && o.Destination == "" {
return fmt.Errorf("destination must be set when using --push")

}

// TODO(jpower432): Validate the reference
return nil
}

func (o *BuildOptions) Run(ctx context.Context) error {
o.Logger.Debugf("Using output directory %q", o.Output)
o.Logger.Infof("Using output directory %q", o.Output)
userSpace, err := workspace.NewLocalWorkspace(o.RootDir)
if err != nil {
return err
Expand Down Expand Up @@ -181,72 +164,13 @@ func (o *BuildOptions) Run(ctx context.Context) error {
if err != nil {
return err
}

if err := templateBuilder.Run(ctx, g, renderSpace); err != nil {
return fmt.Errorf("error building content: %v", err)
}

if o.Push {
// Gather descriptors written to the render directory for publishing
client, err := orasclient.NewClient(
o.Destination,
orasclient.SkipTLSVerify(o.Insecure),
orasclient.WithPlainHTTP(o.PlainHTTP),
orasclient.WithAuthConfigs(o.Configs),
)
if err != nil {
return fmt.Errorf("error configuring client: %v", err)
}
var files []string
err = renderSpace.Walk(func(path string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("traversing %s: %v", path, err)
}
if info == nil {
return fmt.Errorf("no file info")
}

if info.Mode().IsRegular() {
files = append(files, path)
}
return nil
})
if err != nil {
return err
}

// To allow the files to be loaded relative to the render
// workspace, change to the render directory. This is required
// to get path correct in the description annotations.
cwd, err := os.Getwd()
if err != nil {
return err
}
if err := os.Chdir(renderSpace.Path()); err != nil {
return err
}
defer func() {
if err := os.Chdir(cwd); err != nil {
o.Logger.Errorf("%v", err)
}
}()

descs, err := client.GatherDescriptors("", files...)
if err != nil {
return err
}

configDesc, err := client.GenerateConfig(nil)
if err != nil {
return err
}
_, _ = fmt.Fprintf(o.IOStreams.Out, "\nTo publish this content, run the following command:")
_, _ = fmt.Fprintf(o.IOStreams.Out, "\nclient push %s IMAGE\n", o.Output)

if err := client.GenerateManifest(configDesc, nil, descs...); err != nil {
return err
}

if err := client.Execute(ctx); err != nil {
return fmt.Errorf("error publishing content to %s: %v", o.Destination, err)
}
}
return nil
}
93 changes: 59 additions & 34 deletions cli/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import (
"context"
"fmt"
"io/ioutil"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"

"github.com/google/go-containerregistry/pkg/registry"
"github.com/opencontainers/go-digest"
"github.com/stretchr/testify/require"
"github.com/uor-framework/client/cli/log"
"k8s.io/cli-runtime/pkg/genericclioptions"
Expand Down Expand Up @@ -70,29 +69,13 @@ func TestBuildValidate(t *testing.T) {
RootDir: "testdata",
},
},
{
name: "Valid/DestinationWithPush",
opts: &BuildOptions{
Destination: "test-registry.com/client-test:latest",
RootDir: "testdata",
Push: true,
},
},
{
name: "Invalid/RootDirDoesNotExist",
opts: &BuildOptions{
RootDir: "fake",
},
expError: "workspace directory \"fake\": stat fake: no such file or directory",
},
{
name: "Invalid/NoReferenceWithPush",
opts: &BuildOptions{
RootDir: "testdata",
Push: true,
},
expError: "destination must be set when using --push",
},
}

for _, c := range cases {
Expand All @@ -112,14 +95,10 @@ func TestBuildRun(t *testing.T) {
testlogr, err := log.NewLogger(ioutil.Discard, "debug")
require.NoError(t, err)

server := httptest.NewServer(registry.New())
t.Cleanup(server.Close)
u, err := url.Parse(server.URL)
require.NoError(t, err)

type spec struct {
name string
opts *BuildOptions
expected string
expError string
}

Expand All @@ -135,10 +114,9 @@ func TestBuildRun(t *testing.T) {
},
Logger: testlogr,
},
Destination: fmt.Sprintf("%s/client-test:latest", u.Host),
RootDir: "testdata/flatworkspace",
Push: true,
RootDir: "testdata/flatworkspace",
},
expected: "testdata/expected/flatworkspace",
},
{
name: "Success/MultiLevelWorkspace",
Expand All @@ -151,10 +129,9 @@ func TestBuildRun(t *testing.T) {
},
Logger: testlogr,
},
Destination: fmt.Sprintf("%s/client-test:latest", u.Host),
RootDir: "testdata/multi-level-workspace",
Push: true,
RootDir: "testdata/multi-level-workspace",
},
expected: "testdata/expected/multi-level-workspace",
},
{
name: "Success/UORParsing",
Expand All @@ -167,10 +144,24 @@ func TestBuildRun(t *testing.T) {
},
Logger: testlogr,
},
Destination: fmt.Sprintf("%s/client-test:latest", u.Host),
RootDir: "testdata/uor-template",
Push: true,
RootDir: "testdata/uor-template",
},
expected: "testdata/expected/uor-template",
},
{
name: "Failure/TwoRoots",
opts: &BuildOptions{
RootOptions: &RootOptions{
IOStreams: genericclioptions.IOStreams{
Out: os.Stdout,
In: os.Stdin,
ErrOut: os.Stderr,
},
Logger: testlogr,
},
RootDir: "testdata/tworoots",
},
expError: "error building content: error calculating root node: multiple roots found in graph: fish.jpg, fish2.jpg",
},
}

Expand All @@ -182,8 +173,42 @@ func TestBuildRun(t *testing.T) {
require.EqualError(t, err, c.expError)
} else {
require.NoError(t, err)
// TODO(jpower432): verify resulting this image is now pullable
actual := walkDir(t, c.opts.Output)
expected := walkDir(t, c.expected)

for path, data1 := range actual {
t.Log("checking path " + path)
data2, ok := expected[path]
require.True(t, ok)
require.Equal(t, digest.FromBytes(data2).String(), digest.FromBytes(data1).String())
}
}
})
}
}

func walkDir(t *testing.T, dir string) map[string][]byte {
files := map[string][]byte{}
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("traversing %s: %v", path, err)
}
if info == nil {
return fmt.Errorf("no file info")
}

if info.IsDir() {
return nil
}

data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
files[filepath.Base(path)] = data

return nil
})
require.NoError(t, err)
return files
}
Loading