diff --git a/actions/actions_test.go b/actions/actions_test.go index 8570ee4f717c8327064f9c5382e460d68058441e..3721f22b47dfd4b2516334991cab2df887d31e3d 100644 --- a/actions/actions_test.go +++ b/actions/actions_test.go @@ -22,6 +22,7 @@ import ( cmocks "github.com/ksonnet/ksonnet/component/mocks" "github.com/ksonnet/ksonnet/metadata/app/mocks" + "github.com/ksonnet/ksonnet/pkg/registry" rmocks "github.com/ksonnet/ksonnet/pkg/registry/mocks" "github.com/spf13/afero" "github.com/stretchr/testify/require" @@ -68,7 +69,7 @@ func mockNsWithName(name string) *cmocks.Namespace { func mockRegistry(name string) *rmocks.Registry { m := &rmocks.Registry{} m.On("Name").Return(name) - m.On("Protocol").Return("github") + m.On("Protocol").Return(registry.ProtocolGitHub) m.On("URI").Return("github.com/ksonnet/parts/tree/master/incubator") return m diff --git a/actions/pkg_install.go b/actions/pkg_install.go new file mode 100644 index 0000000000000000000000000000000000000000..ac289948401aace6461156c8f1cb0bbe57cb03c6 --- /dev/null +++ b/actions/pkg_install.go @@ -0,0 +1,93 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 actions + +import ( + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/ksonnet/ksonnet/pkg/pkg" + "github.com/ksonnet/ksonnet/pkg/registry" +) + +// DepCacher is a function that caches a dependency.j +type DepCacher func(app.App, pkg.Descriptor, string) error + +// PkgInstallDepCacher sets the dep cacher for pkg install. +func PkgInstallDepCacher(dc DepCacher) PkgInstallOpt { + return func(pi *PkgInstall) { + pi.depCacher = dc + } +} + +// PkgInstallOpt is an option for configuring PkgInstall. +type PkgInstallOpt func(*PkgInstall) + +// RunPkgInstall runs `pkg install` +func RunPkgInstall(ksApp app.App, libName, customName string, opts ...PkgInstallOpt) error { + pi, err := NewPkgInstall(ksApp, libName, customName, opts...) + if err != nil { + return err + } + + return pi.Run() +} + +// PkgInstall lists namespaces. +type PkgInstall struct { + app app.App + libName string + customName string + depCacher DepCacher +} + +// NewPkgInstall creates an instance of PkgInstall. +func NewPkgInstall(ksApp app.App, libName, name string, opts ...PkgInstallOpt) (*PkgInstall, error) { + nl := &PkgInstall{ + app: ksApp, + libName: libName, + customName: name, + depCacher: registry.CacheDependency, + } + + for _, opt := range opts { + opt(nl) + } + + return nl, nil +} + +// Run lists namespaces. +func (pi *PkgInstall) Run() error { + d, customName, err := pi.parseDepSpec() + if err != nil { + return err + } + + return pi.depCacher(pi.app, d, customName) +} + +func (pi *PkgInstall) parseDepSpec() (pkg.Descriptor, string, error) { + d, err := pkg.ParseName(pi.libName) + if err != nil { + return pkg.Descriptor{}, "", err + } + + customName := pi.customName + if customName == "" { + customName = d.Part + } + + return d, customName, nil +} diff --git a/actions/pkg_install_test.go b/actions/pkg_install_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0610df528eb5952c9bc6396b145e1540e68a43a6 --- /dev/null +++ b/actions/pkg_install_test.go @@ -0,0 +1,61 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 actions + +import ( + "testing" + + "github.com/ksonnet/ksonnet/metadata/app" + amocks "github.com/ksonnet/ksonnet/metadata/app/mocks" + "github.com/ksonnet/ksonnet/pkg/pkg" + "github.com/ksonnet/ksonnet/pkg/registry" + "github.com/stretchr/testify/require" +) + +func TestPkgInstall(t *testing.T) { + withApp(t, func(appMock *amocks.App) { + libName := "incubator/apache" + customName := "customName" + + dc := func(a app.App, d pkg.Descriptor, cn string) error { + expectedD := pkg.Descriptor{ + Registry: "incubator", + Part: "apache", + } + require.Equal(t, expectedD, d) + require.Equal(t, "customName", cn) + return nil + } + dcOpt := PkgInstallDepCacher(dc) + + a, err := NewPkgInstall(appMock, libName, customName, dcOpt) + require.NoError(t, err) + + libaries := app.LibraryRefSpecs{} + appMock.On("Libraries").Return(libaries, nil) + + registries := app.RegistryRefSpecs{ + "incubator": &app.RegistryRefSpec{ + Protocol: registry.ProtocolFilesystem, + URI: "file:///tmp", + }, + } + appMock.On("Registries").Return(registries, nil) + + err = a.Run() + require.NoError(t, err) + }) +} diff --git a/actions/pkg_list.go b/actions/pkg_list.go new file mode 100644 index 0000000000000000000000000000000000000000..ec76edf125ee4961015fc74f2948839b9a684478 --- /dev/null +++ b/actions/pkg_list.go @@ -0,0 +1,104 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 actions + +import ( + "io" + "os" + "sort" + "strings" + + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/ksonnet/ksonnet/pkg/registry" + "github.com/ksonnet/ksonnet/pkg/util/table" +) + +const ( + // pkgInstalled denotes a package is installed + pkgInstalled = "*" +) + +// RunPkgList runs `pkg list` +func RunPkgList(ksApp app.App) error { + rl, err := NewPkgList(ksApp) + if err != nil { + return err + } + + return rl.Run() +} + +// PkgList lists available registries +type PkgList struct { + app app.App + rm registry.Manager + out io.Writer +} + +// NewPkgList creates an instance of PkgList +func NewPkgList(ksApp app.App) (*PkgList, error) { + rl := &PkgList{ + app: ksApp, + rm: registry.DefaultManager, + out: os.Stdout, + } + + return rl, nil +} + +// Run runs the env list action. +func (pl *PkgList) Run() error { + registries, err := pl.rm.List(pl.app) + if err != nil { + return err + } + + var rows [][]string + + appLibraries, err := pl.app.Libraries() + if err != nil { + return err + } + + for _, r := range registries { + spec, err := r.FetchRegistrySpec() + if err != nil { + return err + } + + for libName := range spec.Libraries { + row := []string{r.Name(), libName} + _, isInstalled := appLibraries[libName] + if isInstalled { + row = append(row, pkgInstalled) + } + + rows = append(rows, row) + } + } + + sort.Slice(rows, func(i, j int) bool { + nameI := strings.Join([]string{rows[i][0], rows[i][1]}, "-") + nameJ := strings.Join([]string{rows[j][0], rows[j][1]}, "-") + + return nameI < nameJ + }) + + t := table.New(pl.out) + t.SetHeader([]string{"registry", "name", "installed"}) + t.AppendBulk(rows) + return t.Render() +} diff --git a/actions/pkg_list_test.go b/actions/pkg_list_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c8c510edb25bea0740d95d0144f57cdddfaa7af7 --- /dev/null +++ b/actions/pkg_list_test.go @@ -0,0 +1,64 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 actions + +import ( + "bytes" + "testing" + + "github.com/ksonnet/ksonnet/metadata/app" + amocks "github.com/ksonnet/ksonnet/metadata/app/mocks" + "github.com/ksonnet/ksonnet/pkg/registry" + rmocks "github.com/ksonnet/ksonnet/pkg/registry/mocks" + "github.com/stretchr/testify/require" +) + +func TestPkgList(t *testing.T) { + withApp(t, func(appMock *amocks.App) { + libaries := app.LibraryRefSpecs{ + "lib1": &app.LibraryRefSpec{}, + } + + appMock.On("Libraries").Return(libaries, nil) + + a, err := NewPkgList(appMock) + require.NoError(t, err) + + var buf bytes.Buffer + a.out = &buf + + rm := &rmocks.Manager{} + a.rm = rm + + spec := ®istry.Spec{ + Libraries: registry.LibraryRefSpecs{ + "lib1": ®istry.LibraryRef{}, + "lib2": ®istry.LibraryRef{}, + }, + } + + incubator := mockRegistry("incubator") + incubator.On("FetchRegistrySpec").Return(spec, nil) + + registries := []registry.Registry{incubator} + rm.On("List", appMock).Return(registries, nil) + + err = a.Run() + require.NoError(t, err) + + assertOutput(t, "pkg/list/output.txt", buf.String()) + }) +} diff --git a/actions/registry_add.go b/actions/registry_add.go new file mode 100644 index 0000000000000000000000000000000000000000..80d3b3dc74cd8362996f656a14106316e4c70b45 --- /dev/null +++ b/actions/registry_add.go @@ -0,0 +1,81 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 actions + +import ( + "net/url" + "strings" + + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/ksonnet/ksonnet/pkg/registry" +) + +// RunRegistryAdd runs `registry add` +func RunRegistryAdd(ksApp app.App, name, uri, version string) error { + nl, err := NewRegistryAdd(ksApp, name, uri, version) + if err != nil { + return err + } + + return nl.Run() +} + +// RegistryAdd lists namespaces. +type RegistryAdd struct { + app app.App + name string + uri string + version string + + rm registry.Manager +} + +// NewRegistryAdd creates an instance of RegistryAdd. +func NewRegistryAdd(ksApp app.App, name, uri, version string) (*RegistryAdd, error) { + ra := &RegistryAdd{ + app: ksApp, + name: name, + uri: uri, + version: version, + rm: registry.DefaultManager, + } + + return ra, nil +} + +// Run lists namespaces. +func (ra *RegistryAdd) Run() error { + uri, protocol := ra.protocol() + _, err := ra.rm.Add(ra.app, ra.name, protocol, uri, ra.version) + return err +} + +func (ra *RegistryAdd) protocol() (string, string) { + if strings.HasPrefix(ra.uri, "file://") { + return ra.uri, registry.ProtocolFilesystem + } + + if strings.HasPrefix(ra.uri, "/") { + u := url.URL{ + Scheme: "file", + Path: ra.uri, + } + + return u.String(), registry.ProtocolFilesystem + } + + return ra.uri, registry.ProtocolGitHub +} diff --git a/actions/registry_add_test.go b/actions/registry_add_test.go new file mode 100644 index 0000000000000000000000000000000000000000..42b3e07675c0a9aebce54329599856de74bf7bb8 --- /dev/null +++ b/actions/registry_add_test.go @@ -0,0 +1,73 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 actions + +import ( + "testing" + + amocks "github.com/ksonnet/ksonnet/metadata/app/mocks" + "github.com/ksonnet/ksonnet/pkg/registry" + "github.com/ksonnet/ksonnet/pkg/registry/mocks" + "github.com/stretchr/testify/require" +) + +func TestRegistryAdd(t *testing.T) { + withApp(t, func(appMock *amocks.App) { + name := "new" + + cases := []struct { + name string + uri string + version string + expectedURI string + protocol string + }{ + { + name: "github", + uri: "github.com/foo/bar", + expectedURI: "github.com/foo/bar", + protocol: registry.ProtocolGitHub, + }, + { + name: "fs", + uri: "/path", + expectedURI: "file:///path", + protocol: registry.ProtocolFilesystem, + }, + { + name: "fs with URL", + uri: "file:///path", + expectedURI: "file:///path", + protocol: registry.ProtocolFilesystem, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + a, err := NewRegistryAdd(appMock, name, tc.uri, tc.version) + require.NoError(t, err) + + rm := &mocks.Manager{} + rm.On("Add", appMock, "new", tc.protocol, tc.expectedURI, tc.version).Return(nil, nil) + a.rm = rm + + err = a.Run() + require.NoError(t, err) + }) + } + + }) +} diff --git a/actions/registry_list.go b/actions/registry_list.go index 5062e6a1ac0c54b6c130a5c9f1bf098a5c817b67..edd33a78c750332a1e291ac7ec1965f579718965 100644 --- a/actions/registry_list.go +++ b/actions/registry_list.go @@ -54,7 +54,7 @@ func NewRegistryList(ksApp app.App) (*RegistryList, error) { // Run runs the env list action. func (rl *RegistryList) Run() error { - registries, err := rl.rm.Registries(rl.app) + registries, err := rl.rm.List(rl.app) if err != nil { return err } diff --git a/actions/registry_list_test.go b/actions/registry_list_test.go index 30ff5d53e6b057aeee8babdf71b31658851705f4..42f4e70f5a19b75b32146e427f088db2a47ef81b 100644 --- a/actions/registry_list_test.go +++ b/actions/registry_list_test.go @@ -39,7 +39,7 @@ func TestRegistryList(t *testing.T) { registries := []registry.Registry{ mockRegistry("incubator"), } - rm.On("Registries", appMock).Return(registries, nil) + rm.On("List", appMock).Return(registries, nil) err = a.Run() require.NoError(t, err) diff --git a/actions/testdata/pkg/list/output.txt b/actions/testdata/pkg/list/output.txt new file mode 100644 index 0000000000000000000000000000000000000000..e232abcccdb1b65c8ed1e0836147e795de7a2e2f --- /dev/null +++ b/actions/testdata/pkg/list/output.txt @@ -0,0 +1,4 @@ +REGISTRY NAME INSTALLED +======== ==== ========= +incubator lib1 * +incubator lib2 diff --git a/cmd/pkg.go b/cmd/pkg.go index e0ba3bc644c6f9ab650b6c42bf7ea7177fa9a993..38d3ea67067dc8c471f3c2e3cd5a85f2076c39f2 100644 --- a/cmd/pkg.go +++ b/cmd/pkg.go @@ -18,12 +18,10 @@ package cmd import ( "fmt" "os" - "sort" "strings" "github.com/ksonnet/ksonnet/metadata" - "github.com/ksonnet/ksonnet/metadata/parts" - "github.com/ksonnet/ksonnet/pkg/util/table" + "github.com/ksonnet/ksonnet/pkg/parts" "github.com/spf13/cobra" ) @@ -41,10 +39,7 @@ var errInvalidSpec = fmt.Errorf("Command 'pkg install' requires a single argumen func init() { RootCmd.AddCommand(pkgCmd) - pkgCmd.AddCommand(pkgInstallCmd) - pkgCmd.AddCommand(pkgListCmd) pkgCmd.AddCommand(pkgDescribeCmd) - pkgInstallCmd.PersistentFlags().String(flagName, "", "Name to give the dependency, to use within the ksonnet app") } var pkgCmd = &cobra.Command{ @@ -84,67 +79,6 @@ See the annotated file tree below, as an example: }, } -var pkgInstallCmd = &cobra.Command{ - Use: "install <registry>/<library>@<version>", - Short: pkgShortDesc["install"], - Aliases: []string{"get"}, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return fmt.Errorf("Command requires a single argument of the form <registry>/<library>@<version>\n\n%s", cmd.UsageString()) - } - - registry, libID, name, version, err := parseDepSpec(cmd, args[0]) - if err != nil { - return err - } - - cwd, err := os.Getwd() - if err != nil { - return err - } - - manager, err := metadata.Find(cwd) - if err != nil { - return err - } - - _, err = manager.CacheDependency(registry, libID, name, version) - if err != nil { - return err - } - - return nil - }, - Long: ` -The ` + "`install`" + ` command caches a ksonnet library locally, and makes it available -for use in the current ksonnet application. Enough info and metadata is recorded in -` + "`app.yaml` " + `that new users can retrieve the dependency after a fresh clone of this app. - -The library itself needs to be located in a registry (e.g. Github repo). By default, -ksonnet knows about two registries: *incubator* and *stable*, which are the release -channels for official ksonnet libraries. - -### Related Commands - -* ` + "`ks pkg list` " + `— ` + pkgShortDesc["list"] + ` -* ` + "`ks prototype list` " + `— ` + protoShortDesc["list"] + ` -* ` + "`ks registry describe` " + `— ` + regShortDesc["describe"] + ` - -### Syntax -`, - Example: ` -# Install an nginx dependency, based on the latest branch. -# In a ksonnet source file, this can be referenced as: -# local nginx = import "incubator/nginx/nginx.libsonnet"; -ks pkg install incubator/nginx - -# Install an nginx dependency, based on the 'master' branch. -# In a ksonnet source file, this can be referenced as: -# local nginx = import "incubator/nginx/nginx.libsonnet"; -ks pkg install incubator/nginx@master -`, -} - var pkgDescribeCmd = &cobra.Command{ Use: "describe [<registry-name>/]<package-name>", Short: pkgShortDesc["describe"], @@ -218,90 +152,6 @@ known ` + "`<registry-name>`" + ` like *incubator*). The output includes: `, } -var pkgListCmd = &cobra.Command{ - Use: "list", - Short: pkgShortDesc["list"], - RunE: func(cmd *cobra.Command, args []string) error { - const ( - nameHeader = "NAME" - registryHeader = "REGISTRY" - installedHeader = "INSTALLED" - installed = "*" - ) - - if len(args) != 0 { - return fmt.Errorf("Command 'pkg list' does not take arguments") - } - - cwd, err := os.Getwd() - if err != nil { - return err - } - - manager, err := metadata.Find(cwd) - if err != nil { - return err - } - - app, err := manager.App() - if err != nil { - return err - } - - t := table.New(os.Stdout) - t.SetHeader([]string{registryHeader, nameHeader, installedHeader}) - - rows := make([][]string, 0) - for name := range app.Registries() { - reg, _, err := manager.GetRegistry(name) - if err != nil { - return err - } - - for libName := range reg.Libraries { - var row []string - _, isInstalled := app.Libraries()[libName] - if isInstalled { - row = []string{name, libName, installed} - } else { - row = []string{name, libName} - } - - rows = append(rows, row) - } - } - - sort.Slice(rows, func(i, j int) bool { - nameI := strings.Join([]string{rows[i][0], rows[i][1]}, "-") - nameJ := strings.Join([]string{rows[j][0], rows[j][1]}, "-") - - return nameI < nameJ - }) - - t.AppendBulk(rows) - t.Render() - - return nil - }, - Long: ` -The ` + "`list`" + ` command outputs a table that describes all *known* packages (not -necessarily downloaded, but available from existing registries). This includes -the following info: - -1. Library name -2. Registry name -3. Installed status — an asterisk indicates 'installed' - -### Related Commands - -* ` + "`ks pkg install` " + `— ` + pkgShortDesc["install"] + ` -* ` + "`ks pkg describe` " + `— ` + pkgShortDesc["describe"] + ` -* ` + "`ks registry describe` " + `— ` + regShortDesc["describe"] + ` - -### Syntax -`, -} - func parsePkgSpec(spec string) (registry, libID string, err error) { split := strings.SplitN(spec, "/", 2) if len(split) < 2 { @@ -312,31 +162,3 @@ func parsePkgSpec(spec string) (registry, libID string, err error) { libID = strings.SplitN(split[1], "@", 2)[0] return } - -func parseDepSpec(cmd *cobra.Command, spec string) (registry, libID, name, version string, err error) { - registry, libID, err = parsePkgSpec(spec) - if err != nil { - return "", "", "", "", err - } - - split := strings.Split(spec, "@") - if len(split) > 2 { - return "", "", "", "", fmt.Errorf("Symbol '@' is only allowed once, at the end of the argument of the form <registry>/<library>@<version>") - } - version = "" - if len(split) == 2 { - version = split[1] - } - - name, err = cmd.Flags().GetString(flagName) - if err != nil { - return "", "", "", "", err - } else if name == "" { - // Get last component, strip off trailing `@<version>`. - split = strings.Split(spec, "/") - lastComponent := split[len(split)-1] - name = strings.SplitN(lastComponent, "@", 2)[0] - } - - return -} diff --git a/cmd/pkg_install.go b/cmd/pkg_install.go new file mode 100644 index 0000000000000000000000000000000000000000..40100daafb20c5ed394d475f458a07c80821190f --- /dev/null +++ b/cmd/pkg_install.go @@ -0,0 +1,79 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 cmd + +import ( + "fmt" + + "github.com/spf13/viper" + + "github.com/ksonnet/ksonnet/actions" + "github.com/spf13/cobra" +) + +var ( + vPkgInstallName = "pkg-install-name" +) + +var pkgInstallCmd = &cobra.Command{ + Use: "install <registry>/<library>@<version>", + Short: pkgShortDesc["install"], + Aliases: []string{"get"}, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("Command requires a single argument of the form <registry>/<library>@<version>\n\n%s", cmd.UsageString()) + } + + customName := viper.GetString(vPkgInstallName) + + return actions.RunPkgInstall(ka, args[0], customName) + }, + Long: ` +The ` + "`install`" + ` command caches a ksonnet library locally, and makes it available +for use in the current ksonnet application. Enough info and metadata is recorded in +` + "`app.yaml` " + `that new users can retrieve the dependency after a fresh clone of this app. + +The library itself needs to be located in a registry (e.g. Github repo). By default, +ksonnet knows about two registries: *incubator* and *stable*, which are the release +channels for official ksonnet libraries. + +### Related Commands + +* ` + "`ks pkg list` " + `— ` + pkgShortDesc["list"] + ` +* ` + "`ks prototype list` " + `— ` + protoShortDesc["list"] + ` +* ` + "`ks registry describe` " + `— ` + regShortDesc["describe"] + ` + +### Syntax +`, + Example: ` +# Install an nginx dependency, based on the latest branch. +# In a ksonnet source file, this can be referenced as: +# local nginx = import "incubator/nginx/nginx.libsonnet"; +ks pkg install incubator/nginx + +# Install an nginx dependency, based on the 'master' branch. +# In a ksonnet source file, this can be referenced as: +# local nginx = import "incubator/nginx/nginx.libsonnet"; +ks pkg install incubator/nginx@master +`, +} + +func init() { + pkgCmd.AddCommand(pkgInstallCmd) + + pkgInstallCmd.Flags().String(flagName, "", "Name to give the dependency, to use within the ksonnet app") + viper.BindPFlag(vPkgInstallName, pkgInstallCmd.Flags().Lookup(flagName)) +} diff --git a/cmd/pkg_list.go b/cmd/pkg_list.go new file mode 100644 index 0000000000000000000000000000000000000000..41570550df70860e360b5913111458c76d3e0650 --- /dev/null +++ b/cmd/pkg_list.go @@ -0,0 +1,56 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 cmd + +import ( + "fmt" + + "github.com/ksonnet/ksonnet/actions" + "github.com/spf13/cobra" +) + +var pkgListCmd = &cobra.Command{ + Use: "list", + Short: pkgShortDesc["list"], + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return fmt.Errorf("Command 'pkg list' does not take arguments") + } + + return actions.RunPkgList(ka) + }, + Long: ` +The ` + "`list`" + ` command outputs a table that describes all *known* packages (not +necessarily downloaded, but available from existing registries). This includes +the following info: + +1. Library name +2. Registry name +3. Installed status — an asterisk indicates 'installed' + +### Related Commands + +* ` + "`ks pkg install` " + `— ` + pkgShortDesc["install"] + ` +* ` + "`ks pkg describe` " + `— ` + pkgShortDesc["describe"] + ` +* ` + "`ks registry describe` " + `— ` + regShortDesc["describe"] + ` + +### Syntax +`, +} + +func init() { + pkgCmd.AddCommand(pkgListCmd) +} diff --git a/cmd/registry.go b/cmd/registry.go index 8939b685cb55b59f4821ef85d4d8d8b8cdb2403b..9c3785e9b41eb9ae4f538dd9a4440af9796e5a7c 100644 --- a/cmd/registry.go +++ b/cmd/registry.go @@ -22,7 +22,6 @@ import ( "strings" "github.com/ksonnet/ksonnet/metadata" - "github.com/ksonnet/ksonnet/pkg/kubecfg" "github.com/spf13/cobra" ) @@ -38,7 +37,6 @@ var regShortDesc = map[string]string{ func init() { RootCmd.AddCommand(registryCmd) - registryCmd.AddCommand(registryListCmd) registryCmd.AddCommand(registryDescribeCmd) registryCmd.AddCommand(registryAddCmd) @@ -97,7 +95,11 @@ var registryDescribeCmd = &cobra.Command{ return err } - regRef, exists := app.Registries()[name] + appRegistries, err := app.Registries() + if err != nil { + return err + } + regRef, exists := appRegistries[name] if !exists { return fmt.Errorf("Registry '%s' doesn't exist", name) } @@ -145,59 +147,3 @@ by ` + "`<registry-name>`" + `. Specifically, it displays the following: ### Syntax `, } - -var registryAddCmd = &cobra.Command{ - Use: "add <registry-name> <registry-uri>", - Short: regShortDesc["add"], - RunE: func(cmd *cobra.Command, args []string) error { - flags := cmd.Flags() - - if len(args) != 2 { - return fmt.Errorf("Command 'registry add' takes two arguments, which is the name and the repository address of the registry to add") - } - - name := args[0] - uri := args[1] - - version, err := flags.GetString(flagRegistryVersion) - if err != nil { - return err - } - - // TODO allow protocol to be specified by flag once there is greater - // support for other protocol types. - return kubecfg.NewRegistryAddCmd(name, "github", uri, version).Run() - }, - - Long: ` -The ` + "`add`" + ` command allows custom registries to be added to your ksonnet app, -provided that their file structures follow the appropriate schema. *You can look -at the ` + "`incubator`" + ` repo (https://github.com/ksonnet/parts/tree/master/incubator) -as an example.* - -A registry is uniquely identified by its: - -1. Name (e.g. ` + "`incubator`" + `) -2. Version (e.g. ` + "`master`" + `) - -Currently, only registries supporting the **GitHub protocol** can be added. - -During creation, all registries must specify a unique name and URI where the -registry lives. Optionally, a version can be provided (e.g. the *Github branch -name*). If a version is not specified, it will default to ` + "`latest`" + `. - - -### Related Commands - -* ` + "`ks registry list` " + `— ` + regShortDesc["list"] + ` - -### Syntax -`, - Example: `# Add a registry with the name 'databases' at the uri 'github.com/example' -ks registry add databases github.com/example - -# Add a registry with the name 'databases' at the uri -# 'github.com/example/tree/master/reg' and the version (branch name) 0.0.1 -# NOTE that "0.0.1" overrides the branch name in the URI ("master") -ks registry add databases github.com/example/tree/master/reg --version=0.0.1`, -} diff --git a/cmd/registry_add.go b/cmd/registry_add.go new file mode 100644 index 0000000000000000000000000000000000000000..df3e3c629c7c223d5bdb960a58a9f83a2432a906 --- /dev/null +++ b/cmd/registry_add.go @@ -0,0 +1,77 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 cmd + +import ( + "fmt" + + "github.com/ksonnet/ksonnet/actions" + "github.com/spf13/cobra" +) + +var registryAddCmd = &cobra.Command{ + Use: "add <registry-name> <registry-uri>", + Short: regShortDesc["add"], + RunE: func(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + + if len(args) != 2 { + return fmt.Errorf("Command 'registry add' takes two arguments, which is the name and the repository address of the registry to add") + } + + name := args[0] + uri := args[1] + + version, err := flags.GetString(flagRegistryVersion) + if err != nil { + return err + } + + return actions.RunRegistryAdd(ka, name, uri, version) + }, + + Long: ` +The ` + "`add`" + ` command allows custom registries to be added to your ksonnet app, +provided that their file structures follow the appropriate schema. *You can look +at the ` + "`incubator`" + ` repo (https://github.com/ksonnet/parts/tree/master/incubator) +as an example.* + +A registry is uniquely identified by its: + +1. Name (e.g. ` + "`incubator`" + `) +2. Version (e.g. ` + "`master`" + `) + +Currently, only registries supporting the **GitHub protocol** can be added. + +During creation, all registries must specify a unique name and URI where the +registry lives. Optionally, a version can be provided (e.g. the *Github branch +name*). If a version is not specified, it will default to ` + "`latest`" + `. + + +### Related Commands + +* ` + "`ks registry list` " + `— ` + regShortDesc["list"] + ` + +### Syntax +`, + Example: `# Add a registry with the name 'databases' at the uri 'github.com/example' +ks registry add databases github.com/example + +# Add a registry with the name 'databases' at the uri +# 'github.com/example/tree/master/reg' and the version (branch name) 0.0.1 +# NOTE that "0.0.1" overrides the branch name in the URI ("master") +ks registry add databases github.com/example/tree/master/reg --version=0.0.1`, +} diff --git a/cmd/registry_list.go b/cmd/registry_list.go index e0b222bdb4837b3415c1fbbf67396a76a7f2b43e..abfe3def5248961aeb25fd353b2cc0de9b5fd189 100644 --- a/cmd/registry_list.go +++ b/cmd/registry_list.go @@ -47,3 +47,7 @@ table includes the following info: ### Syntax `, } + +func init() { + registryCmd.AddCommand(registryListCmd) +} diff --git a/e2e/app.go b/e2e/app.go index baa51e73e0dc98ff564d3f8f09f00bb190839d19..f69ec6c2456b2257c6400022e898914c13671dd8 100644 --- a/e2e/app.go +++ b/e2e/app.go @@ -63,6 +63,13 @@ func (a *app) paramList(args ...string) *output { return o } +func (a *app) pkgList() *output { + o := a.runKs("pkg", "list") + assertExitStatus(o, 0) + + return o +} + func (a *app) paramSet(key, value string, args ...string) *output { o := a.runKs(append([]string{"param", "set", key, value}, args...)...) assertExitStatus(o, 0) @@ -70,6 +77,20 @@ func (a *app) paramSet(key, value string, args ...string) *output { return o } +func (a *app) registryAdd(registryName, uri string) *output { + o := a.runKs("registry", "add", registryName, uri) + assertExitStatus(o, 0) + + return o +} + +func (a *app) registryList(args ...string) *output { + o := a.runKs(append([]string{"registry", "list"}, args...)...) + assertExitStatus(o, 0) + + return o +} + func (a *app) generateDeployedService() { appDir := a.dir diff --git a/e2e/helpers.go b/e2e/helpers.go index 624c2f00727f96ee815c1568742f51ffdfdb78f1..2eb93605a64663a1bdcb16d9e2f8fe0c0775330a 100644 --- a/e2e/helpers.go +++ b/e2e/helpers.go @@ -16,7 +16,10 @@ package e2e import ( + "bytes" + "html/template" "io/ioutil" + "net/url" "os" "path/filepath" "strings" @@ -77,3 +80,34 @@ func assertContents(name, path string) { "expected output to be:\n%s\nit was:\n%s\n", expected, got) } + +func assertTemplate(data interface{}, name, output string) { + path := filepath.Join("testdata", "output", name) + ExpectWithOffset(1, path).To(BeAnExistingFile()) + + t, err := template.ParseFiles(path) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + + var buf bytes.Buffer + err = t.Execute(&buf, data) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + + expected := buf.String() + got := output + ExpectWithOffset(1, expected).To(Equal(got), + "expected output to be:\n%s\nit was:\n%s\n", + expected, got) +} + +func convertPathToURI(path string) string { + if strings.HasPrefix(path, "file://") { + return path + } + + u := url.URL{ + Scheme: "file", + Path: path, + } + + return u.String() +} diff --git a/e2e/pkg_test.go b/e2e/pkg_test.go index 94b58a80c287f27f65a901ff3d8afe9e55647893..8dd1b615fa90379a6074ea302df9f7d11c4ce0ab 100644 --- a/e2e/pkg_test.go +++ b/e2e/pkg_test.go @@ -31,7 +31,7 @@ var _ = Describe("ks pkg", func() { a = e.initApp("") }) - Describe("describe", func() { + Describe("add", func() { Context("incubator/apache", func() { It("describes the package", func() { o := a.runKs("pkg", "describe", "incubator/apache") @@ -41,16 +41,49 @@ var _ = Describe("ks pkg", func() { }) }) - Describe("install", func() { + Describe("describe", func() { Context("incubator/apache", func() { It("describes the package", func() { - o := a.runKs("pkg", "install", "incubator/apache") + o := a.runKs("pkg", "describe", "incubator/apache") assertExitStatus(o, 0) + assertOutput("pkg/describe/output.txt", o.stdout) + }) + }) + }) + + Describe("install", func() { + Context("github based part", func() { + Context("incubator/apache", func() { + It("describes the package", func() { + o := a.runKs("pkg", "install", "incubator/apache") + assertExitStatus(o, 0) - pkgDir := filepath.Join(a.dir, "vendor", "incubator", "apache") - Expect(pkgDir).To(BeADirectory()) + pkgDir := filepath.Join(a.dir, "vendor", "incubator", "apache") + Expect(pkgDir).To(BeADirectory()) + }) }) }) + + Context("fs based part", func() { + Context("local/contour", func() { + It("describes the package", func() { + path, err := filepath.Abs(filepath.Join("testdata", "registries", "parts-infra")) + Expect(err).ToNot(HaveOccurred()) + + o := a.registryAdd("local", path) + + o = a.runKs("pkg", "install", "local/contour") + assertExitStatus(o, 0) + + o = a.pkgList() + + m := map[string]interface{}{} + tPath := filepath.Join("pkg", "install", "fs-output.txt.tmpl") + assertTemplate(m, tPath, o.stdout) + }) + }) + }) + }) Describe("list", func() { diff --git a/e2e/registry_test.go b/e2e/registry_test.go index 44fa9b94f43307fd7bbb972e974e0322ad3075f0..7dbd3a105d4423f9e7424974bb81b2b561aa0049 100644 --- a/e2e/registry_test.go +++ b/e2e/registry_test.go @@ -18,7 +18,10 @@ package e2e import ( + "path/filepath" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) var _ = Describe("ks registry", func() { @@ -29,6 +32,46 @@ var _ = Describe("ks registry", func() { a.generateDeployedService() }) + Describe("add", func() { + + var add = func(path string) { + o := a.runKs("registry", "add", "local", path) + assertExitStatus(o, 0) + + uri := convertPathToURI(path) + m := map[string]interface{}{ + "uri": uri, + } + + o = a.registryList() + + tPath := filepath.Join("registry", "add", "output.txt.tmpl") + assertTemplate(m, tPath, o.stdout) + } + + Context("a filesystem based registry", func() { + Context("as a path", func() { + It("adds a registry", func() { + path, err := filepath.Abs(filepath.Join("testdata", "registries", "parts-infra")) + Expect(err).ToNot(HaveOccurred()) + + add(path) + }) + }) + Context("as a URL", func() { + It("adds a registry", func() { + path, err := filepath.Abs(filepath.Join("testdata", "registries", "parts-infra")) + Expect(err).ToNot(HaveOccurred()) + + uri := convertPathToURI(path) + + add(uri) + }) + }) + + }) + }) + Describe("list", func() { It("lists the currently configured registries", func() { o := a.runKs("registry", "list") diff --git a/e2e/testdata/output/pkg/install/fs-output.txt.tmpl b/e2e/testdata/output/pkg/install/fs-output.txt.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..dcd990f489316df151157239a9c71f1f439671cf --- /dev/null +++ b/e2e/testdata/output/pkg/install/fs-output.txt.tmpl @@ -0,0 +1,14 @@ +REGISTRY NAME INSTALLED +======== ==== ========= +incubator apache +incubator efk +incubator mariadb +incubator memcached +incubator mongodb +incubator mysql +incubator nginx +incubator node +incubator postgres +incubator redis +incubator tomcat +local contour * diff --git a/e2e/testdata/output/registry/add/output.txt.tmpl b/e2e/testdata/output/registry/add/output.txt.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..26c0114ba1a4dbeabdaf343387ab88081a41e00f --- /dev/null +++ b/e2e/testdata/output/registry/add/output.txt.tmpl @@ -0,0 +1,4 @@ +NAME PROTOCOL URI +==== ======== === +incubator github github.com/ksonnet/parts/tree/master/incubator +local fs {{ .uri }} diff --git a/e2e/testdata/registries/parts-infra/contour/parts.yaml b/e2e/testdata/registries/parts-infra/contour/parts.yaml new file mode 100644 index 0000000000000000000000000000000000000000..29a9240e38f0024acc019ef37ab0495ec52bcad6 --- /dev/null +++ b/e2e/testdata/registries/parts-infra/contour/parts.yaml @@ -0,0 +1,12 @@ +name: contour +apiVersion: 0.0.1 +kind: ksonnet.io/parts +description: > + Contour is a Kubernetes ingress controller for Lyft's Envoy proxy. +author: Bryan Liles <bryan@heptio.com> +quickStart: + prototype: io.foo.bar + componentName: contour + flags: + comment: Logging stack that processes input from fluentd. +license: Apache 2.0 diff --git a/e2e/testdata/registries/parts-infra/contour/prototypes/simple.jsonnet b/e2e/testdata/registries/parts-infra/contour/prototypes/simple.jsonnet new file mode 100644 index 0000000000000000000000000000000000000000..b4ccc02ad3b42cb2ae0fa9162175defe49022298 --- /dev/null +++ b/e2e/testdata/registries/parts-infra/contour/prototypes/simple.jsonnet @@ -0,0 +1,6 @@ +// @apiVersion 0.0.1 +// @name foo.bar.baz.contour +// @description Something fancy +// @shortDescription short description + +{} \ No newline at end of file diff --git a/e2e/testdata/registries/parts-infra/registry.yaml b/e2e/testdata/registries/parts-infra/registry.yaml new file mode 100644 index 0000000000000000000000000000000000000000..172726d1a0d11d994e3cdda609a06331e34ab985 --- /dev/null +++ b/e2e/testdata/registries/parts-infra/registry.yaml @@ -0,0 +1,5 @@ +apiVersion: 0.1.0 +kind: ksonnet.io/registry +libraries: + contour: + path: contour \ No newline at end of file diff --git a/metadata/app/app.go b/metadata/app/app.go index ff67825727ae13a46b555ce1eb6dfac94dcca03b..663742fc44e26722f33fb587d2e46851b6b655d5 100644 --- a/metadata/app/app.go +++ b/metadata/app/app.go @@ -50,18 +50,20 @@ var ( // App is a ksonnet application. type App interface { AddEnvironment(name, k8sSpecFlag string, spec *EnvironmentSpec) error + AddRegistry(spec *RegistryRefSpec) error Environment(name string) (*EnvironmentSpec, error) Environments() (EnvironmentSpecs, error) EnvironmentParams(name string) (string, error) Fs() afero.Fs Init() error LibPath(envName string) (string, error) - Libraries() LibraryRefSpecs - Registries() RegistryRefSpecs + Libraries() (LibraryRefSpecs, error) + Registries() (RegistryRefSpecs, error) RemoveEnvironment(name string) error RenameEnvironment(from, to string) error Root() string UpdateTargets(envName string, targets []string) error + UpdateLib(name string, spec *LibraryRefSpec) error Upgrade(dryRun bool) error } @@ -77,6 +79,37 @@ func newBaseApp(fs afero.Fs, root string) *baseApp { } } +func (ba *baseApp) AddRegistry(newReg *RegistryRefSpec) error { + spec, err := ba.load() + if err != nil { + return err + } + + if newReg.Name == "" { + return ErrRegistryNameInvalid + } + + _, exists := spec.Registries[newReg.Name] + if exists { + return ErrRegistryExists + } + + spec.Registries[newReg.Name] = newReg + + return ba.save(spec) +} + +func (ba *baseApp) UpdateLib(name string, libSpec *LibraryRefSpec) error { + spec, err := ba.load() + if err != nil { + return err + } + + spec.Libraries[name] = libSpec + + return ba.save(spec) +} + func (ba *baseApp) Fs() afero.Fs { return ba.fs } @@ -93,7 +126,6 @@ func (ba *baseApp) EnvironmentParams(envName string) (string, error) { } return string(b), nil - } // Load loads the application configuration. @@ -107,10 +139,23 @@ func Load(fs afero.Fs, appRoot string) (App, error) { default: return nil, errors.Errorf("unknown apiVersion %q in %s", spec.APIVersion, appYamlName) case "0.0.1": - return NewApp001(fs, appRoot) + return NewApp001(fs, appRoot), nil case "0.1.0": - return NewApp010(fs, appRoot) + return NewApp010(fs, appRoot), nil + } +} + +func (ba *baseApp) save(spec *Spec) error { + return Write(ba.fs, ba.root, spec) +} + +func (ba *baseApp) load() (*Spec, error) { + spec, err := Read(ba.fs, ba.root) + if err != nil { + return nil, err } + + return spec, nil } func updateLibData(fs afero.Fs, k8sSpecFlag, libPath string, useVersionPath bool) (string, error) { diff --git a/metadata/app/app001.go b/metadata/app/app001.go index 470c19d6e51dd578c4691d04802afb50425610d7..8396ff563cb58978b4d4f8dc2a710763d6693d72 100644 --- a/metadata/app/app001.go +++ b/metadata/app/app001.go @@ -35,27 +35,20 @@ const ( // App001 is a ksonnet 0.0.1 application. type App001 struct { - spec *Spec - out io.Writer + out io.Writer *baseApp } var _ App = (*App001)(nil) // NewApp001 creates an App001 instance. -func NewApp001(fs afero.Fs, root string) (*App001, error) { - spec, err := Read(fs, root) - if err != nil { - return nil, err - } - +func NewApp001(fs afero.Fs, root string) *App001 { ba := newBaseApp(fs, root) return &App001{ - spec: spec, out: os.Stdout, baseApp: ba, - }, nil + } } // AddEnvironment adds an environment spec to the app spec. If the spec already exists, @@ -135,13 +128,22 @@ func (a *App001) LibPath(envName string) (string, error) { } // Libraries returns application libraries. -func (a *App001) Libraries() LibraryRefSpecs { - return a.spec.Libraries +func (a *App001) Libraries() (LibraryRefSpecs, error) { + spec, err := a.load() + if err != nil { + return nil, err + } + return spec.Libraries, nil } // Registries returns application registries. -func (a *App001) Registries() RegistryRefSpecs { - return a.spec.Registries +func (a *App001) Registries() (RegistryRefSpecs, error) { + spec, err := a.load() + if err != nil { + return nil, err + } + + return spec.Registries, nil } // RemoveEnvironment removes an environment. @@ -163,7 +165,8 @@ func (a *App001) UpdateTargets(envName string, targets []string) error { // Upgrade upgrades the app to the latest apiVersion. func (a *App001) Upgrade(dryRun bool) error { - if err := a.load(); err != nil { + spec, err := a.load() + if err != nil { return err } @@ -183,10 +186,10 @@ func (a *App001) Upgrade(dryRun bool) error { a.convertEnvironment(env.Path, dryRun) } - a.spec.APIVersion = "0.1.0" + spec.APIVersion = "0.1.0" if dryRun { - data, err := a.spec.Marshal() + data, err := spec.Marshal() if err != nil { return err } @@ -196,7 +199,7 @@ func (a *App001) Upgrade(dryRun bool) error { return nil } - return a.save() + return a.save(spec) } type k8sSchema struct { @@ -253,7 +256,12 @@ func (a *App001) convertEnvironment(envName string, dryRun bool) error { return err } - a.spec.Environments[envName] = env + spec, err := a.load() + if err != nil { + return err + } + + spec.Environments[envName] = env if dryRun { fmt.Fprintf(a.out, "[dry run]\t* adding the environment description in environment `%s to `app.yaml`.\n", @@ -267,25 +275,15 @@ func (a *App001) convertEnvironment(envName string, dryRun bool) error { k8sSpecFlag := fmt.Sprintf("version:%s", env.KubernetesVersion) _, err = LibUpdater(a.fs, k8sSpecFlag, app010LibPath(a.root), true) - return err -} - -func (a *App001) appLibPath(envName string) string { - return filepath.Join(a.root, EnvironmentDirName, envName, ".metadata") -} - -func (a *App001) save() error { - return Write(a.fs, a.root, a.spec) -} - -func (a *App001) load() error { - spec, err := Read(a.fs, a.root) if err != nil { return err } - a.spec = spec - return nil + return a.save(spec) +} + +func (a *App001) appLibPath(envName string) string { + return filepath.Join(a.root, EnvironmentDirName, envName, ".metadata") } func (a *App001) envDir(envName string) string { diff --git a/metadata/app/app001_test.go b/metadata/app/app001_test.go index 0c780924e919819f03b7e508ebf1db7881423352..e9d4989fb8d495e6df18d860a184e402825696a7 100644 --- a/metadata/app/app001_test.go +++ b/metadata/app/app001_test.go @@ -291,7 +291,6 @@ func withApp001Fs(t *testing.T, appName string, fn func(app *App001)) { stageFile(t, fs, appName, "/app.yaml") - app, err := NewApp001(fs, "/") - require.NoError(t, err) + app := NewApp001(fs, "/") fn(app) } diff --git a/metadata/app/app010.go b/metadata/app/app010.go index d4cdc08c9746646d76af4b8d898e001a87328fd0..380fa393469ea770436f62c9d7c160dadb96d936 100644 --- a/metadata/app/app010.go +++ b/metadata/app/app010.go @@ -29,35 +29,31 @@ import ( // App010 is a ksonnet 0.1.0 application. type App010 struct { - spec *Spec *baseApp } var _ App = (*App010)(nil) // NewApp010 creates an App010 instance. -func NewApp010(fs afero.Fs, root string) (*App010, error) { +func NewApp010(fs afero.Fs, root string) *App010 { ba := newBaseApp(fs, root) a := &App010{ baseApp: ba, } - if err := a.load(); err != nil { - return nil, err - } - - return a, nil + return a } // AddEnvironment adds an environment spec to the app spec. If the spec already exists, // it is overwritten. -func (a *App010) AddEnvironment(name, k8sSpecFlag string, spec *EnvironmentSpec) error { - if err := a.load(); err != nil { +func (a *App010) AddEnvironment(name, k8sSpecFlag string, newEnv *EnvironmentSpec) error { + spec, err := a.load() + if err != nil { return err } - a.spec.Environments[name] = spec + spec.Environments[name] = newEnv if k8sSpecFlag != "" { ver, err := LibUpdater(a.fs, k8sSpecFlag, app010LibPath(a.root), true) @@ -65,15 +61,20 @@ func (a *App010) AddEnvironment(name, k8sSpecFlag string, spec *EnvironmentSpec) return err } - a.spec.Environments[name].KubernetesVersion = ver + spec.Environments[name].KubernetesVersion = ver } - return a.save() + return a.save(spec) } // Environment returns the spec for an environment. func (a *App010) Environment(name string) (*EnvironmentSpec, error) { - s, ok := a.spec.Environments[name] + spec, err := a.load() + if err != nil { + return nil, err + } + + s, ok := spec.Environments[name] if !ok { return nil, errors.Errorf("environment %q was not found", name) } @@ -83,7 +84,12 @@ func (a *App010) Environment(name string) (*EnvironmentSpec, error) { // Environments returns all environment specs. func (a *App010) Environments() (EnvironmentSpecs, error) { - return a.spec.Environments, nil + spec, err := a.load() + if err != nil { + return nil, err + } + + return spec.Environments, nil } // Init initializes the App. @@ -123,22 +129,33 @@ func (a *App010) LibPath(envName string) (string, error) { } // Libraries returns application libraries. -func (a *App010) Libraries() LibraryRefSpecs { - return a.spec.Libraries +func (a *App010) Libraries() (LibraryRefSpecs, error) { + spec, err := a.load() + if err != nil { + return nil, err + } + + return spec.Libraries, nil } // Registries returns application registries. -func (a *App010) Registries() RegistryRefSpecs { - return a.spec.Registries +func (a *App010) Registries() (RegistryRefSpecs, error) { + spec, err := a.load() + if err != nil { + return nil, err + } + + return spec.Registries, nil } // RemoveEnvironment removes an environment. func (a *App010) RemoveEnvironment(envName string) error { - if err := a.load(); err != nil { + spec, err := a.load() + if err != nil { return err } - delete(a.spec.Environments, envName) - return a.save() + delete(spec.Environments, envName) + return a.save(spec) } // RenameEnvironment renames environments. @@ -147,16 +164,17 @@ func (a *App010) RenameEnvironment(from, to string) error { return err } - if err := a.load(); err != nil { + spec, err := a.load() + if err != nil { return err } - a.spec.Environments[to] = a.spec.Environments[from] - delete(a.spec.Environments, from) + spec.Environments[to] = spec.Environments[from] + delete(spec.Environments, from) - a.spec.Environments[to].Path = to + spec.Environments[to].Path = to - return a.save() + return a.save(spec) } // UpdateTargets updates the list of targets for a 0.1.0 application. @@ -203,17 +221,3 @@ func (a *App010) findLegacySpec() ([]string, error) { return found, nil } - -func (a *App010) save() error { - return Write(a.fs, a.root, a.spec) -} - -func (a *App010) load() error { - spec, err := Read(a.fs, a.root) - if err != nil { - return err - } - - a.spec = spec - return nil -} diff --git a/metadata/app/app010_test.go b/metadata/app/app010_test.go index dbccc236bec1ad960ef0445e7a45a90d6d34282c..7f812fb05d192de83e2bf9cb7860911c8b40650e 100644 --- a/metadata/app/app010_test.go +++ b/metadata/app/app010_test.go @@ -317,8 +317,7 @@ func withApp010Fs(t *testing.T, appName string, fn func(app *App010)) { stageFile(t, fs, appName, "/app.yaml") - app, err := NewApp010(fs, "/") - require.NoError(t, err) + app := NewApp010(fs, "/") fn(app) } diff --git a/metadata/app/mocks/App.go b/metadata/app/mocks/App.go index e24c8061030a8617f57da125e4c4c1c0c6b56633..bef8ec9873980d8915497f19935be653ea1aadf8 100644 --- a/metadata/app/mocks/App.go +++ b/metadata/app/mocks/App.go @@ -39,6 +39,20 @@ func (_m *App) AddEnvironment(name string, k8sSpecFlag string, spec *app.Environ return r0 } +// AddRegistry provides a mock function with given fields: spec +func (_m *App) AddRegistry(spec *app.RegistryRefSpec) error { + ret := _m.Called(spec) + + var r0 error + if rf, ok := ret.Get(0).(func(*app.RegistryRefSpec) error); ok { + r0 = rf(spec) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Environment provides a mock function with given fields: name func (_m *App) Environment(name string) (*app.EnvironmentSpec, error) { ret := _m.Called(name) @@ -158,7 +172,7 @@ func (_m *App) LibPath(envName string) (string, error) { } // Libraries provides a mock function with given fields: -func (_m *App) Libraries() app.LibraryRefSpecs { +func (_m *App) Libraries() (app.LibraryRefSpecs, error) { ret := _m.Called() var r0 app.LibraryRefSpecs @@ -170,11 +184,18 @@ func (_m *App) Libraries() app.LibraryRefSpecs { } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // Registries provides a mock function with given fields: -func (_m *App) Registries() app.RegistryRefSpecs { +func (_m *App) Registries() (app.RegistryRefSpecs, error) { ret := _m.Called() var r0 app.RegistryRefSpecs @@ -186,7 +207,14 @@ func (_m *App) Registries() app.RegistryRefSpecs { } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // RemoveEnvironment provides a mock function with given fields: name @@ -231,6 +259,20 @@ func (_m *App) Root() string { return r0 } +// UpdateLib provides a mock function with given fields: name, spec +func (_m *App) UpdateLib(name string, spec *app.LibraryRefSpec) error { + ret := _m.Called(name, spec) + + var r0 error + if rf, ok := ret.Get(0).(func(string, *app.LibraryRefSpec) error); ok { + r0 = rf(name, spec) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpdateTargets provides a mock function with given fields: envName, targets func (_m *App) UpdateTargets(envName string, targets []string) error { ret := _m.Called(envName, targets) diff --git a/metadata/interface.go b/metadata/interface.go index 21a2e7d23fd83db8df1f8eff62e6d5f4b698f32b..0cecd9202f30b1836f3bc5ed42f31fdc2c6ce487 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -22,8 +22,8 @@ import ( "github.com/ksonnet/ksonnet/env" "github.com/ksonnet/ksonnet/metadata/app" param "github.com/ksonnet/ksonnet/metadata/params" - "github.com/ksonnet/ksonnet/metadata/parts" - "github.com/ksonnet/ksonnet/metadata/registry" + "github.com/ksonnet/ksonnet/pkg/parts" + "github.com/ksonnet/ksonnet/pkg/registry" "github.com/ksonnet/ksonnet/prototype" "github.com/spf13/afero" ) @@ -68,10 +68,8 @@ type Manager interface { GetDestination(envName string) (env.Destination, error) // Dependency/registry API. - AddRegistry(name, protocol, uri, version string) (*registry.Spec, error) GetRegistry(name string) (*registry.Spec, string, error) GetPackage(registryName, libID string) (*parts.Spec, error) - CacheDependency(registryName, libID, libName, libVersion string) (*parts.Spec, error) GetDependency(libName string) (*parts.Spec, error) GetAllPrototypes() (prototype.SpecificationSchemas, error) } @@ -92,9 +90,9 @@ func Init(name, rootPath string, k8sSpecFlag, serverURI, namespace *string) (Man defaultIncubatorURI = "github.com/ksonnet/parts/tree/master/" + defaultIncubatorRegName ) - gh, err := makeGitHubRegistryManager(&app.RegistryRefSpec{ + gh, err := registry.NewGitHub(&app.RegistryRefSpec{ Name: "incubator", - Protocol: "github", + Protocol: registry.ProtocolGitHub, URI: defaultIncubatorURI, }) if err != nil { diff --git a/metadata/manager.go b/metadata/manager.go index 022efea53aa16997fcb7e2d2fb4ff473f189ed5d..bf5b888020123e2058e32491b9ab63fb0e58beca 100644 --- a/metadata/manager.go +++ b/metadata/manager.go @@ -24,7 +24,7 @@ import ( "github.com/ksonnet/ksonnet/component" "github.com/ksonnet/ksonnet/env" "github.com/ksonnet/ksonnet/metadata/app" - "github.com/ksonnet/ksonnet/metadata/registry" + "github.com/ksonnet/ksonnet/pkg/registry" str "github.com/ksonnet/ksonnet/strings" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -115,7 +115,7 @@ func findManager(p string, appFS afero.Fs) (*manager, error) { } } -func initManager(name, rootPath string, k8sSpecFlag, serverURI, namespace *string, incubatorReg registry.Manager, appFS afero.Fs) (*manager, error) { +func initManager(name, rootPath string, k8sSpecFlag, serverURI, namespace *string, incubatorReg registry.Registry, appFS afero.Fs) (*manager, error) { m, err := newManager(rootPath, appFS) if err != nil { return nil, errors.Wrap(err, "create manager") @@ -231,7 +231,7 @@ func (m *manager) createUserDirTree() error { return nil } -func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte, gh registry.Manager) error { +func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte, gh registry.Registry) error { exists, err := afero.DirExists(m.appFS, m.rootPath) if err != nil { return fmt.Errorf("Could not check existence of directory '%s':\n%v", m.rootPath, err) @@ -289,7 +289,7 @@ func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte, return nil } -func generateRegistryYAMLData(incubatorReg registry.Manager) ([]byte, error) { +func generateRegistryYAMLData(incubatorReg registry.Registry) ([]byte, error) { regSpec, err := incubatorReg.FetchRegistrySpec() if err != nil { return nil, err diff --git a/metadata/registry.go b/metadata/registry.go index 99df3ed59416b24542f2894cb4cdfd122b968657..eb70fbaf9a9326e5042c7e17472fe74757fa74a2 100644 --- a/metadata/registry.go +++ b/metadata/registry.go @@ -21,64 +21,20 @@ import ( "path/filepath" "github.com/ksonnet/ksonnet/metadata/app" - "github.com/ksonnet/ksonnet/metadata/parts" - "github.com/ksonnet/ksonnet/metadata/registry" + "github.com/ksonnet/ksonnet/pkg/parts" + "github.com/ksonnet/ksonnet/pkg/registry" "github.com/ksonnet/ksonnet/prototype" str "github.com/ksonnet/ksonnet/strings" - log "github.com/sirupsen/logrus" "github.com/spf13/afero" ) -// AddRegistry adds a registry with `name`, `protocol`, and `uri` to -// the current ksonnet application. -func (m *manager) AddRegistry(name, protocol, uri, version string) (*registry.Spec, error) { - appSpec, err := app.Read(m.appFS, m.rootPath) - if err != nil { - return nil, err - } - - // Add registry reference to app spec. - registryManager, err := makeGitHubRegistryManager(&app.RegistryRefSpec{ - Name: name, - Protocol: protocol, - URI: uri, - }) - if err != nil { - return nil, err - } - - err = appSpec.AddRegistryRef(registryManager.RegistryRefSpec) - if err != nil { - return nil, err - } - - // Retrieve the contents of registry. - registrySpec, err := m.getOrCacheRegistry(registryManager) - if err != nil { - return nil, err - } - - // Write registry specification back out to app specification. - specBytes, err := appSpec.Marshal() - if err != nil { - return nil, err - } - - err = afero.WriteFile(m.appFS, m.appYAMLPath, specBytes, defaultFilePermissions) - if err != nil { - return nil, err - } - - return registrySpec, nil -} - func (m *manager) GetRegistry(name string) (*registry.Spec, string, error) { - registryManager, protocol, err := m.getRegistryManager(name) + r, protocol, err := m.getRegistryManager(name) if err != nil { return nil, "", err } - regSpec, exists, err := m.registrySpecFromFile(m.registryPath(registryManager)) + regSpec, exists, err := m.registrySpecFromFile(m.registryPath(r)) if !exists { return nil, "", fmt.Errorf("Registry '%s' does not exist", name) } else if err != nil { @@ -97,7 +53,7 @@ func (m *manager) GetPackage(registryName, libID string) (*parts.Spec, error) { regRefSpec, ok := appSpec.GetRegistryRef(registryName) if !ok { - return nil, fmt.Errorf("COuld not find registry '%s'", registryName) + return nil, fmt.Errorf("Could not find registry '%s'", registryName) } registryManager, _, err := m.getRegistryManagerFor(regRefSpec) @@ -157,79 +113,6 @@ func (m *manager) GetDependency(libName string) (*parts.Spec, error) { return partsSpec, nil } -func (m *manager) CacheDependency(registryName, libID, libName, libVersion string) (*parts.Spec, error) { - // Retrieve application specification. - appSpec, err := app.Read(m.appFS, m.rootPath) - if err != nil { - return nil, err - } - - if _, ok := appSpec.Libraries[libName]; ok { - return nil, fmt.Errorf("Package '%s' already exists. Use the --name flag to install this package with a unique identifier", libName) - } - - // Retrieve registry manager for this specific registry. - regRefSpec, exists := appSpec.GetRegistryRef(registryName) - if !exists { - return nil, fmt.Errorf("Registry '%s' does not exist", registryName) - } - - registryManager, _, err := m.getRegistryManagerFor(regRefSpec) - if err != nil { - return nil, err - } - - // Get all directories and files first, then write to disk. This - // protects us from failing with a half-cached dependency because of - // a network failure. - directories := []string{} - files := map[string][]byte{} - parts, libRef, err := registryManager.ResolveLibrary( - libID, - libName, - libVersion, - func(relPath string, contents []byte) error { - files[str.AppendToPath(m.vendorPath, relPath)] = contents - return nil - }, - func(relPath string) error { - directories = append(directories, str.AppendToPath(m.vendorPath, relPath)) - return nil - }) - if err != nil { - return nil, err - } - - // Add library to app specification, but wait to write it out until - // the end, in case one of the network calls fails. - appSpec.Libraries[libName] = libRef - appSpecData, err := appSpec.Marshal() - if err != nil { - return nil, err - } - - log.Infof("Retrieved %d files", len(files)) - - for _, dir := range directories { - if err := m.appFS.MkdirAll(dir, defaultFolderPermissions); err != nil { - return nil, err - } - } - - for path, content := range files { - if err := afero.WriteFile(m.appFS, path, content, defaultFilePermissions); err != nil { - return nil, err - } - } - - err = afero.WriteFile(m.appFS, m.appYAMLPath, appSpecData, defaultFilePermissions) - if err != nil { - return nil, err - } - - return parts, nil -} - func (m *manager) GetPrototypesForDependency(registryName, libID string) (prototype.SpecificationSchemas, error) { // TODO: Remove `registryName` when we flatten vendor/. specs := prototype.SpecificationSchemas{} @@ -286,15 +169,19 @@ func (m *manager) GetAllPrototypes() (prototype.SpecificationSchemas, error) { return specs, nil } -func (m *manager) registryDir(regManager registry.Manager) string { +func (m *manager) registryDir(regManager registry.Registry) string { return str.AppendToPath(m.registriesPath, regManager.RegistrySpecDir()) } -func (m *manager) registryPath(regManager registry.Manager) string { +func (m *manager) registryPath(regManager registry.Registry) string { + path := regManager.RegistrySpecFilePath() + if filepath.IsAbs(path) { + return path + } return str.AppendToPath(m.registriesPath, regManager.RegistrySpecFilePath()) } -func (m *manager) getRegistryManager(registryName string) (registry.Manager, string, error) { +func (m *manager) getRegistryManager(registryName string) (registry.Registry, string, error) { appSpec, err := app.Read(m.appFS, m.rootPath) if err != nil { return nil, "", err @@ -308,24 +195,18 @@ func (m *manager) getRegistryManager(registryName string) (registry.Manager, str return m.getRegistryManagerFor(regRefSpec) } -func (m *manager) getRegistryManagerFor(registryRefSpec *app.RegistryRefSpec) (registry.Manager, string, error) { - var err error - var manager registry.Manager - var protocol string - - switch registryRefSpec.Protocol { - case "github": - manager, err = makeGitHubRegistryManager(registryRefSpec) - protocol = "github" - default: - return nil, "", fmt.Errorf("Invalid protocol '%s'", registryRefSpec.Protocol) +func (m *manager) getRegistryManagerFor(registryRefSpec *app.RegistryRefSpec) (registry.Registry, string, error) { + a, err := m.App() + if err != nil { + return nil, "", err } + r, err := registry.Locate(a, registryRefSpec) if err != nil { return nil, "", err } - return manager, protocol, nil + return r, r.Protocol(), nil } func (m *manager) registrySpecFromFile(path string) (*registry.Spec, bool, error) { @@ -357,39 +238,3 @@ func (m *manager) registrySpecFromFile(path string) (*registry.Spec, bool, error return nil, false, nil } - -func (m *manager) getOrCacheRegistry(gh registry.Manager) (*registry.Spec, error) { - // Check local disk cache. - registrySpecFile := m.registryPath(gh) - registrySpec, exists, err := m.registrySpecFromFile(registrySpecFile) - if !exists { - // If failed, use the protocol to try to retrieve app specification. - registrySpec, err = gh.FetchRegistrySpec() - if err != nil { - return nil, err - } - - registrySpecBytes, err := registrySpec.Marshal() - if err != nil { - return nil, err - } - - // NOTE: We call mkdir after getting the registry spec, since a - // network call might fail and leave this half-initialized empty - // directory. - registrySpecDir := str.AppendToPath(m.registriesPath, gh.RegistrySpecDir()) - err = m.appFS.MkdirAll(registrySpecDir, defaultFolderPermissions) - if err != nil { - return nil, err - } - - err = afero.WriteFile(m.appFS, registrySpecFile, registrySpecBytes, defaultFilePermissions) - if err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } - - return registrySpec, nil -} diff --git a/metadata/registry_managers.go b/metadata/registry_managers.go deleted file mode 100644 index 6719be79d8bacb9cc74559a4609c9879915ab1c0..0000000000000000000000000000000000000000 --- a/metadata/registry_managers.go +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright 2018 The kubecfg 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 -// -// http://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 metadata - -import ( - "context" - "fmt" - "net/http" - "net/url" - "os" - "path" - "strings" - - "github.com/google/go-github/github" - "github.com/ksonnet/ksonnet/metadata/app" - "github.com/ksonnet/ksonnet/metadata/parts" - "github.com/ksonnet/ksonnet/metadata/registry" - "golang.org/x/oauth2" -) - -const ( - rawGitHubRoot = "https://raw.githubusercontent.com" - defaultGitHubBranch = "master" - - uriField = "uri" - refSpecField = "refSpec" - resolvedSHAField = "resolvedSHA" -) - -var ( - errInvalidURI = fmt.Errorf("Invalid GitHub URI: try navigating in GitHub to the URI of the folder containing the 'registry.yaml', and using that URI instead. Generally, this URI should be of the form 'github.com/{organization}/{repository}/tree/{branch}/[path-to-directory]'") -) - -// -// GitHub registry manager. -// - -type gitHubRegistryManager struct { - *app.RegistryRefSpec - registryDir string - org string - repo string - registryRepoPath string - registrySpecRepoPath string -} - -func makeGithubClient() *github.Client { - var hc *http.Client - - ght := os.Getenv("GITHUB_TOKEN") - if len(ght) > 0 { - ctx := context.Background() - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: ght}, - ) - hc = oauth2.NewClient(ctx, ts) - } - - return github.NewClient(hc) -} - -func makeGitHubRegistryManager(registryRef *app.RegistryRefSpec) (*gitHubRegistryManager, error) { - gh := gitHubRegistryManager{RegistryRefSpec: registryRef} - - var err error - - // Set registry path. - gh.registryDir = gh.Name - - // Parse GitHub URI. - var refspec string - gh.org, gh.repo, refspec, gh.registryRepoPath, gh.registrySpecRepoPath, err = parseGitHubURI(gh.URI) - if err != nil { - return nil, err - } - - // Resolve the refspec to a commit SHA. - client := makeGithubClient() - ctx := context.Background() - - sha, _, err := client.Repositories.GetCommitSHA1(ctx, gh.org, gh.repo, refspec, "") - if err != nil { - return nil, err - } - gh.GitVersion = &app.GitVersionSpec{ - RefSpec: refspec, - CommitSHA: sha, - } - - return &gh, nil -} - -func (gh *gitHubRegistryManager) RegistrySpecDir() string { - return gh.registryDir -} - -func (gh *gitHubRegistryManager) RegistrySpecFilePath() string { - if gh.GitVersion.CommitSHA != "" { - return path.Join(gh.registryDir, gh.GitVersion.CommitSHA+".yaml") - } - return path.Join(gh.registryDir, gh.GitVersion.RefSpec+".yaml") -} - -func (gh *gitHubRegistryManager) FetchRegistrySpec() (*registry.Spec, error) { - // Fetch app spec at specific commit. - client := makeGithubClient() - ctx := context.Background() - - // Get contents. - getOpts := github.RepositoryContentGetOptions{Ref: gh.GitVersion.CommitSHA} - file, _, _, err := client.Repositories.GetContents(ctx, gh.org, gh.repo, gh.registrySpecRepoPath, &getOpts) - if file == nil { - return nil, fmt.Errorf("Could not find valid registry at uri '%s/%s/%s' and refspec '%s' (resolves to sha '%s')", gh.org, gh.repo, gh.registrySpecRepoPath, gh.GitVersion.RefSpec, gh.GitVersion.CommitSHA) - } else if err != nil { - return nil, err - } - - registrySpecText, err := file.GetContent() - if err != nil { - return nil, err - } - - // Deserialize, return. - registrySpec, err := registry.Unmarshal([]byte(registrySpecText)) - if err != nil { - return nil, err - } - - registrySpec.GitVersion = &app.GitVersionSpec{ - RefSpec: gh.GitVersion.RefSpec, - CommitSHA: gh.GitVersion.CommitSHA, - } - - return registrySpec, nil -} - -func (gh *gitHubRegistryManager) MakeRegistryRefSpec() *app.RegistryRefSpec { - return gh.RegistryRefSpec -} - -func (gh *gitHubRegistryManager) ResolveLibrarySpec(libID, libRefSpec string) (*parts.Spec, error) { - client := makeGithubClient() - - // Resolve `version` (a git refspec) to a specific SHA. - ctx := context.Background() - resolvedSHA, _, err := client.Repositories.GetCommitSHA1(ctx, gh.org, gh.repo, libRefSpec, "") - if err != nil { - return nil, err - } - - // Resolve app spec. - appSpecPath := strings.Join([]string{gh.registryRepoPath, libID, partsYAMLFile}, "/") - ctx = context.Background() - getOpts := &github.RepositoryContentGetOptions{Ref: resolvedSHA} - file, directory, _, err := client.Repositories.GetContents(ctx, gh.org, gh.repo, appSpecPath, getOpts) - if err != nil { - return nil, err - } else if directory != nil { - return nil, fmt.Errorf("Can't download library specification; resource '%s' points at a file", gh.registrySpecRawURL()) - } - - partsSpecText, err := file.GetContent() - if err != nil { - return nil, err - } - - parts, err := parts.Unmarshal([]byte(partsSpecText)) - if err != nil { - return nil, err - } - - return parts, nil -} - -func (gh *gitHubRegistryManager) ResolveLibrary(libID, libAlias, libRefSpec string, onFile registry.ResolveFile, onDir registry.ResolveDirectory) (*parts.Spec, *app.LibraryRefSpec, error) { - const ( - defaultRefSpec = "master" - ) - - client := makeGithubClient() - - // Resolve `version` (a git refspec) to a specific SHA. - ctx := context.Background() - if len(libRefSpec) == 0 { - libRefSpec = defaultRefSpec - } - resolvedSHA, _, err := client.Repositories.GetCommitSHA1(ctx, gh.org, gh.repo, libRefSpec, "") - if err != nil { - return nil, nil, err - } - - // Resolve directories and files. - path := strings.Join([]string{gh.registryRepoPath, libID}, "/") - err = gh.resolveDir(client, libID, path, resolvedSHA, onFile, onDir) - if err != nil { - return nil, nil, err - } - - // Resolve app spec. - appSpecPath := strings.Join([]string{path, partsYAMLFile}, "/") - ctx = context.Background() - getOpts := &github.RepositoryContentGetOptions{Ref: resolvedSHA} - file, directory, _, err := client.Repositories.GetContents(ctx, gh.org, gh.repo, appSpecPath, getOpts) - if err != nil { - return nil, nil, err - } else if directory != nil { - return nil, nil, fmt.Errorf("Can't download library specification; resource '%s' points at a file", gh.registrySpecRawURL()) - } - - partsSpecText, err := file.GetContent() - if err != nil { - return nil, nil, err - } - - parts, err := parts.Unmarshal([]byte(partsSpecText)) - if err != nil { - return nil, nil, err - } - - refSpec := app.LibraryRefSpec{ - Name: libAlias, - Registry: gh.Name, - GitVersion: &app.GitVersionSpec{ - RefSpec: libRefSpec, - CommitSHA: resolvedSHA, - }, - } - - return parts, &refSpec, nil -} - -func (gh *gitHubRegistryManager) resolveDir(client *github.Client, libID, path, version string, onFile registry.ResolveFile, onDir registry.ResolveDirectory) error { - ctx := context.Background() - getOpts := &github.RepositoryContentGetOptions{Ref: version} - file, directory, _, err := client.Repositories.GetContents(ctx, gh.org, gh.repo, path, getOpts) - if err != nil { - return err - } else if file != nil { - return fmt.Errorf("Lib ID '%s' resolves to a file in registry '%s'", libID, gh.Name) - } - - for _, item := range directory { - switch item.GetType() { - case "file": - itemPath := item.GetPath() - file, directory, _, err := client.Repositories.GetContents(ctx, gh.org, gh.repo, itemPath, getOpts) - if err != nil { - return err - } else if directory != nil { - return fmt.Errorf("INTERNAL ERROR: GitHub API reported resource '%s' of type file, but returned type dir", itemPath) - } - contents, err := file.GetContent() - if err != nil { - return err - } - if err := onFile(itemPath, []byte(contents)); err != nil { - return err - } - case "dir": - itemPath := item.GetPath() - if err := onDir(itemPath); err != nil { - return err - } - if err := gh.resolveDir(client, libID, itemPath, version, onFile, onDir); err != nil { - return err - } - case "symlink": - case "submodule": - return fmt.Errorf("Invalid library '%s'; ksonnet doesn't support libraries with symlinks or submodules", libID) - } - } - - return nil -} - -func (gh *gitHubRegistryManager) registrySpecRawURL() string { - return strings.Join([]string{rawGitHubRoot, gh.org, gh.repo, gh.GitVersion.RefSpec, gh.registrySpecRepoPath}, "/") -} - -// -// Helper functions. -// - -func parseGitHubURI(uri string) (org, repo, refSpec, regRepoPath, regSpecRepoPath string, err error) { - // Normalize URI. - uri = strings.TrimSpace(uri) - if strings.HasPrefix(uri, "http://github.com") || strings.HasPrefix(uri, "https://github.com") || strings.HasPrefix(uri, "http://www.github.com") || strings.HasPrefix(uri, "https://www.github.com") { - // Do nothing. - } else if strings.HasPrefix(uri, "github.com") || strings.HasPrefix(uri, "www.github.com") { - uri = "http://" + uri - } else { - return "", "", "", "", "", fmt.Errorf("Registries using protocol 'github' must provide URIs beginning with 'github.com' (optionally prefaced with 'http', 'https', 'www', and so on") - } - - parsed, err := url.Parse(uri) - if err != nil { - return "", "", "", "", "", err - } - - if len(parsed.Query()) != 0 { - return "", "", "", "", "", fmt.Errorf("No query strings allowed in registry URI:\n%s", uri) - } - - components := strings.Split(parsed.Path, "/") - if len(components) < 3 { - return "", "", "", "", "", fmt.Errorf("GitHub URI must point at a respository:\n%s", uri) - } - - // NOTE: The first component is always blank, because the path - // begins like: '/whatever'. - org = components[1] - repo = components[2] - - // - // Parse out `regSpecRepoPath`. There are a few cases: - // * URI points at a directory inside the respoitory, e.g., - // 'http://github.com/ksonnet/parts/tree/master/incubator' - // * URI points at an 'app.yaml', e.g., - // 'http://github.com/ksonnet/parts/blob/master/registry.yaml' - // * URI points at a repository root, e.g., - // 'http://github.com/ksonnet/parts' - // - if len := len(components); len > 4 { - refSpec = components[4] - - // - // Case where we're pointing at either a directory inside a GitHub - // URL, or an 'app.yaml' inside a GitHub URL. - // - - // See note above about first component being blank. - if components[3] == "tree" { - // If we have a trailing '/' character, last component will be blank. Make - // sure that `regRepoPath` does not contain a trailing `/`. - if components[len-1] == "" { - regRepoPath = strings.Join(components[5:len-1], "/") - components[len-1] = registryYAMLFile - } else { - regRepoPath = strings.Join(components[5:], "/") - components = append(components, registryYAMLFile) - } - regSpecRepoPath = strings.Join(components[5:], "/") - return - } else if components[3] == "blob" && components[len-1] == registryYAMLFile { - regRepoPath = strings.Join(components[5:len-1], "/") - // Path to the `registry.yaml` (may or may not exist). - regSpecRepoPath = strings.Join(components[5:], "/") - return - } else { - return "", "", "", "", "", errInvalidURI - } - } else { - refSpec = defaultGitHubBranch - - // Else, URI should point at repository root. - if components[len-1] == "" { - components[len-1] = defaultGitHubBranch - components = append(components, registryYAMLFile) - } else { - components = append(components, defaultGitHubBranch, registryYAMLFile) - } - - regRepoPath = "" - regSpecRepoPath = registryYAMLFile - return - } -} diff --git a/metadata/registry_test.go b/metadata/registry_test.go index 01cef9b8b94c6a2100937dd5453e217b0e09069c..2e6d281f09cd3e6004ea85459fbc1950b1c0212f 100644 --- a/metadata/registry_test.go +++ b/metadata/registry_test.go @@ -17,160 +17,12 @@ package metadata import ( "path" - "testing" "github.com/ksonnet/ksonnet/metadata/app" - "github.com/ksonnet/ksonnet/metadata/parts" - "github.com/ksonnet/ksonnet/metadata/registry" + "github.com/ksonnet/ksonnet/pkg/parts" + "github.com/ksonnet/ksonnet/pkg/registry" ) -func TestParseGiHubRegistryURITest(t *testing.T) { - tests := []struct { - // Specification to parse. - uri string - - // Optional error to check. - targetErr error - - // Optional results to verify. - targetOrg string - targetRepo string - targetRefSpec string - targetRegistryRepoPath string - targetRegistrySpecRepoPath string - }{ - // - // `parseGitHubURI` should correctly parse org, repo, and refspec. Does not - // test path parsing. - // - { - uri: "github.com/exampleOrg1/exampleRepo1", - - targetOrg: "exampleOrg1", - targetRepo: "exampleRepo1", - targetRefSpec: "master", - targetRegistryRepoPath: "", - targetRegistrySpecRepoPath: "registry.yaml", - }, - { - uri: "github.com/exampleOrg2/exampleRepo2/tree/master", - - targetOrg: "exampleOrg2", - targetRepo: "exampleRepo2", - targetRefSpec: "master", - targetRegistryRepoPath: "", - targetRegistrySpecRepoPath: "registry.yaml", - }, - { - uri: "github.com/exampleOrg3/exampleRepo3/tree/exampleBranch1", - - targetOrg: "exampleOrg3", - targetRepo: "exampleRepo3", - targetRefSpec: "exampleBranch1", - targetRegistryRepoPath: "", - targetRegistrySpecRepoPath: "registry.yaml", - }, - { - // Fails because `blob` refers to a file, but this refers to a directory. - uri: "github.com/exampleOrg4/exampleRepo4/blob/master", - targetErr: errInvalidURI, - }, - { - uri: "github.com/exampleOrg4/exampleRepo4/tree/exampleBranch2", - - targetOrg: "exampleOrg4", - targetRepo: "exampleRepo4", - targetRefSpec: "exampleBranch2", - targetRegistryRepoPath: "", - targetRegistrySpecRepoPath: "registry.yaml", - }, - - // - // Parsing URIs with paths. - // - { - // Fails because referring to a directory requires a URI with - // `tree/{branchName}` prepending the path. - uri: "github.com/exampleOrg6/exampleRepo6/path/to/some/registry", - targetErr: errInvalidURI, - }, - { - uri: "github.com/exampleOrg5/exampleRepo5/tree/master/path/to/some/registry", - - targetOrg: "exampleOrg5", - targetRepo: "exampleRepo5", - targetRefSpec: "master", - targetRegistryRepoPath: "path/to/some/registry", - targetRegistrySpecRepoPath: "path/to/some/registry/registry.yaml", - }, - { - uri: "github.com/exampleOrg6/exampleRepo6/tree/exampleBranch3/path/to/some/registry", - - targetOrg: "exampleOrg6", - targetRepo: "exampleRepo6", - targetRefSpec: "exampleBranch3", - targetRegistryRepoPath: "path/to/some/registry", - targetRegistrySpecRepoPath: "path/to/some/registry/registry.yaml", - }, - { - // Fails because `blob` refers to a file, but this refers to a directory. - uri: "github.com/exampleOrg7/exampleRepo7/blob/master", - targetErr: errInvalidURI, - }, - { - // Fails because `blob` refers to a file, but this refers to a directory. - uri: "github.com/exampleOrg5/exampleRepo5/blob/exampleBranch2", - targetErr: errInvalidURI, - }, - } - - for _, test := range tests { - // Make sure we correctly parse each URN as a bare-domain URI, as well as - // with 'http://' and 'https://' as prefixes. - for _, prefix := range []string{"http://", "https://", "http://www.", "https://www.", "www.", ""} { - // Make sure we correctly parse each URI even if it has the optional - // trailing `/` character. - for _, suffix := range []string{"/", ""} { - uri := prefix + test.uri + suffix - - t.Run(uri, func(t *testing.T) { - org, repo, refspec, registryRepoPath, registrySpecRepoPath, err := parseGitHubURI(uri) - if test.targetErr != nil { - if err != test.targetErr { - t.Fatalf("Expected URI '%s' parse to fail with err '%v', got: '%v'", uri, test.targetErr, err) - } - return - } - - if err != nil { - t.Fatalf("Expected parse to succeed, but failed with error '%v'", err) - } - - if org != test.targetOrg { - t.Errorf("Expected org '%s', got '%s'", test.targetOrg, org) - } - - if repo != test.targetRepo { - t.Errorf("Expected repo '%s', got '%s'", test.targetRepo, repo) - } - - if refspec != test.targetRefSpec { - t.Errorf("Expected refspec '%s', got '%s'", test.targetRefSpec, refspec) - } - - if registryRepoPath != test.targetRegistryRepoPath { - t.Errorf("Expected registryRepoPath '%s', got '%s'", test.targetRegistryRepoPath, registryRepoPath) - } - - if registrySpecRepoPath != test.targetRegistrySpecRepoPath { - t.Errorf("Expected targetRegistrySpecRepoPath '%s', got '%s'", test.targetRegistrySpecRepoPath, registrySpecRepoPath) - } - }) - } - } - } -} - // // Mock registry manager for end-to-end tests. // @@ -180,6 +32,8 @@ type mockRegistryManager struct { registryDir string } +var _ registry.Registry = (*mockRegistryManager)(nil) + func newMockRegistryManager(name string) *mockRegistryManager { return &mockRegistryManager{ registryDir: name, @@ -189,6 +43,18 @@ func newMockRegistryManager(name string) *mockRegistryManager { } } +func (m *mockRegistryManager) Name() string { + return m.registryDir +} + +func (m *mockRegistryManager) Protocol() string { + return registry.ProtocolGitHub +} + +func (m *mockRegistryManager) URI() string { + return "github.com/foo/bar" +} + func (m *mockRegistryManager) ResolveLibrarySpec(libID, libRefSpec string) (*parts.Spec, error) { return nil, nil } diff --git a/pkg/kubecfg/registry.go b/pkg/kubecfg/registry.go deleted file mode 100644 index 4de55f786de3052d2a76b5719f66aa08b16a6ae7..0000000000000000000000000000000000000000 --- a/pkg/kubecfg/registry.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2017 The ksonnet 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 -// -// http://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 kubecfg - -// RegistryAddCmd contains the metadata needed to create a registry. -type RegistryAddCmd struct { - name string - protocol string - uri string - version string -} - -// NewRegistryAddCmd initializes a RegistryAddCmd. -func NewRegistryAddCmd(name, protocol, uri, version string) *RegistryAddCmd { - if version == "" { - version = "latest" - } - - return &RegistryAddCmd{name: name, protocol: protocol, uri: uri, version: version} -} - -// Run adds the registry to the ksonnet project. -func (c *RegistryAddCmd) Run() error { - manager, err := manager() - if err != nil { - return err - } - - _, err = manager.AddRegistry(c.name, c.protocol, c.uri, c.version) - return err -} diff --git a/metadata/parts/schema.go b/pkg/parts/schema.go similarity index 100% rename from metadata/parts/schema.go rename to pkg/parts/schema.go diff --git a/metadata/parts/schema_test.go b/pkg/parts/schema_test.go similarity index 100% rename from metadata/parts/schema_test.go rename to pkg/parts/schema_test.go diff --git a/pkg/pkg/name.go b/pkg/pkg/name.go new file mode 100644 index 0000000000000000000000000000000000000000..1fb22e7e1fd648e0040e0c235eca8c9ffc7a1b09 --- /dev/null +++ b/pkg/pkg/name.go @@ -0,0 +1,58 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 pkg + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" +) + +var errInvalidSpec = fmt.Errorf("package name should be in the form `<registry>/<library>@<version>`") + +// Descriptor describes a package. +type Descriptor struct { + Registry string + Part string + Version string +} + +// ParseName parses a package name into its components +func ParseName(name string) (Descriptor, error) { + split := strings.SplitN(name, "/", 2) + if len(split) < 2 { + return Descriptor{}, errInvalidSpec + } + + registryName := split[0] + partName := strings.SplitN(split[1], "@", 2)[0] + + split = strings.Split(name, "@") + if len(split) > 2 { + return Descriptor{}, errors.Errorf("symbol '@' is only allowed once, at the end of the argument of the form <registry>/<library>@<version>") + } + var version string + if len(split) == 2 { + version = split[1] + } + + return Descriptor{ + Registry: registryName, + Part: partName, + Version: version, + }, nil +} diff --git a/pkg/pkg/name_test.go b/pkg/pkg/name_test.go new file mode 100644 index 0000000000000000000000000000000000000000..000390d8ab9e6a621914ee13c734510972ab6680 --- /dev/null +++ b/pkg/pkg/name_test.go @@ -0,0 +1,59 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 pkg + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_ParseName(t *testing.T) { + cases := []struct { + name string + expected Descriptor + isErr bool + }{ + { + name: "parts-infra/contour", + expected: Descriptor{Registry: "parts-infra", Part: "contour"}, + }, + { + name: "parts-infra/contour@0.1.0", + expected: Descriptor{Registry: "parts-infra", Part: "contour", Version: "0.1.0"}, + }, + { + name: "invalid", + isErr: true, + }, + { + name: "foo/bar@baz@doh", + isErr: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + d, err := ParseName(tc.name) + if tc.isErr { + require.Error(t, err) + return + } + + require.Equal(t, tc.expected, d) + }) + } +} diff --git a/pkg/registry/add.go b/pkg/registry/add.go new file mode 100644 index 0000000000000000000000000000000000000000..58d7b2189b1299ec6b3d10319de99758cc08465d --- /dev/null +++ b/pkg/registry/add.go @@ -0,0 +1,135 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 registry + +import ( + "path/filepath" + + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/pkg/errors" + "github.com/spf13/afero" +) + +// Add adds a registry with `name`, `protocol`, and `uri` to +// the current ksonnet application. +func Add(a app.App, name, protocol, uri, version string) (*Spec, error) { + var r Registry + var err error + + initSpec := &app.RegistryRefSpec{ + Name: name, + Protocol: protocol, + URI: uri, + } + + switch protocol { + case ProtocolGitHub: + r, err = githubFactory(initSpec) + case ProtocolFilesystem: + r, err = NewFs(a, initSpec) + default: + return nil, errors.Errorf("invalid registry protocol %q", protocol) + } + + if err != nil { + return nil, err + } + + err = a.AddRegistry(r.MakeRegistryRefSpec()) + if err != nil { + return nil, err + } + + // Retrieve the contents of registry. + registrySpec, err := getOrCacheRegistry(a, r) + if err != nil { + return nil, errors.Wrap(err, "cache registry") + } + + return registrySpec, nil +} + +func getOrCacheRegistry(a app.App, gh Registry) (*Spec, error) { + // Check local disk cache. + registrySpecFile := makePath(a, gh) + registrySpec, exists, err := load(a, registrySpecFile) + if err != nil { + return nil, errors.Wrap(err, "load registry spec file") + } + + if !exists { + // If failed, use the protocol to try to retrieve app specification. + registrySpec, err = gh.FetchRegistrySpec() + if err != nil { + return nil, err + } + + registrySpecBytes, err := registrySpec.Marshal() + if err != nil { + return nil, err + } + + // NOTE: We call mkdir after getting the registry spec, since a + // network call might fail and leave this half-initialized empty + // directory. + registrySpecDir := filepath.Join(root(a), gh.RegistrySpecDir()) + err = a.Fs().MkdirAll(registrySpecDir, app.DefaultFolderPermissions) + if err != nil { + return nil, err + } + + err = afero.WriteFile(a.Fs(), registrySpecFile, registrySpecBytes, app.DefaultFilePermissions) + if err != nil { + return nil, err + } + } else if err != nil { + return nil, err + } + + return registrySpec, nil +} + +func load(a app.App, path string) (*Spec, bool, error) { + exists, err := afero.Exists(a.Fs(), path) + if err != nil { + return nil, false, errors.Wrapf(err, "check if %q exists", path) + } + + // NOTE: case where directory of the same name exists should be + // fine, most filesystems allow you to have a directory and file of + // the same name. + if exists { + isDir, err := afero.IsDir(a.Fs(), path) + if err != nil { + return nil, false, errors.Wrapf(err, "check if %q is a dir", path) + } + + if !isDir { + registrySpecBytes, err := afero.ReadFile(a.Fs(), path) + if err != nil { + return nil, false, err + } + + registrySpec, err := Unmarshal(registrySpecBytes) + if err != nil { + return nil, false, err + } + return registrySpec, true, nil + } + } + + return nil, false, nil +} diff --git a/pkg/registry/add_test.go b/pkg/registry/add_test.go new file mode 100644 index 0000000000000000000000000000000000000000..897489d56db7675b093fabfeade925edabca5e2b --- /dev/null +++ b/pkg/registry/add_test.go @@ -0,0 +1,148 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 registry + +import ( + "path/filepath" + "testing" + + "github.com/spf13/afero" + + "github.com/ksonnet/ksonnet/metadata/app" + amocks "github.com/ksonnet/ksonnet/metadata/app/mocks" + ghutil "github.com/ksonnet/ksonnet/pkg/util/github" + "github.com/ksonnet/ksonnet/pkg/util/github/mocks" + "github.com/ksonnet/ksonnet/pkg/util/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func withMockManager(t *testing.T, fn func(*amocks.App, afero.Fs)) { + fs := afero.NewMemMapFs() + appMock := &amocks.App{} + appMock.On("Fs").Return(fs) + appMock.On("Root").Return("/app") + appMock.On("LibPath", mock.AnythingOfType("string")).Return(filepath.Join("/app", "lib", "v1.8.7"), nil) + + fn(appMock, fs) +} + +func TestAdd(t *testing.T) { + withMockManager(t, func(appMock *amocks.App, fs afero.Fs) { + expectedSpec := &app.RegistryRefSpec{ + Name: "new", + Protocol: ProtocolGitHub, + URI: "github.com/foo/bar", + GitVersion: &app.GitVersionSpec{ + CommitSHA: "40285d8a14f1ac5787e405e1023cf0c07f6aa28c", + RefSpec: "master", + }, + } + + appMock.On("AddRegistry", expectedSpec).Return(nil) + + ghMock := &mocks.GitHub{} + ghMock.On("CommitSHA1", mock.Anything, mock.Anything, "master").Return("40285d8a14f1ac5787e405e1023cf0c07f6aa28c", nil) + + registryContent := buildContent(t, registryYAMLFile) + ghMock.On( + "Contents", + mock.Anything, + ghutil.Repo{Org: "foo", Repo: "bar"}, + registryYAMLFile, + "40285d8a14f1ac5787e405e1023cf0c07f6aa28c"). + Return(registryContent, nil, nil) + + ghOpt := GitHubClient(ghMock) + ogGHFactory := githubFactory + defer func(fn func(registryRef *app.RegistryRefSpec) (*GitHub, error)) { + githubFactory = fn + }(ogGHFactory) + + githubFactory = func(registryRef *app.RegistryRefSpec) (*GitHub, error) { + return NewGitHub(registryRef, ghOpt) + } + + spec, err := Add(appMock, "new", ProtocolGitHub, "github.com/foo/bar", "") + require.NoError(t, err) + + require.Equal(t, registrySpec, spec) + + }) +} + +func Test_load(t *testing.T) { + withMockManager(t, func(appMock *amocks.App, fs afero.Fs) { + + test.StageFile(t, fs, "registry.yaml", "/app/registry.yaml") + test.StageFile(t, fs, "invalid-registry.yaml", "/app/invalid-registry.yaml") + + cases := []struct { + name string + path string + isErr bool + expected *Spec + exists bool + }{ + { + name: "file exists", + path: "/app/registry.yaml", + expected: registrySpec, + exists: true, + }, + { + name: "file is not valid", + path: "/app/invalid-registry.yaml", + isErr: true, + exists: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s, exists, err := load(appMock, tc.path) + + if tc.isErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.expected, s) + assert.Equal(t, tc.exists, exists) + }) + } + + }) +} + +var ( + registrySpec = &Spec{ + APIVersion: "0.1.0", + Kind: "ksonnet.io/registry", + GitVersion: &app.GitVersionSpec{ + RefSpec: "master", + CommitSHA: "40285d8a14f1ac5787e405e1023cf0c07f6aa28c", + }, + Libraries: LibraryRefSpecs{ + "apache": &LibraryRef{ + Version: "master", + Path: "apache", + }, + }, + } +) diff --git a/pkg/registry/cache.go b/pkg/registry/cache.go new file mode 100644 index 0000000000000000000000000000000000000000..d8144e60625bfb57f1f928e66c5c4779cc938a31 --- /dev/null +++ b/pkg/registry/cache.go @@ -0,0 +1,99 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 registry + +import ( + "fmt" + "path/filepath" + + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/ksonnet/ksonnet/pkg/pkg" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +// CacheDependency caches registry dependencies. +// TODO: create unit tests for this once mocks for this package are +// worked out. +func CacheDependency(a app.App, d pkg.Descriptor, customName string) error { + libs, err := a.Libraries() + if err != nil { + return err + } + + if _, ok := libs[customName]; ok { + return errors.Errorf("package '%s' already exists. Use the --name flag to install this package with a unique identifier", + customName) + } + + registries, err := a.Registries() + if err != nil { + return err + } + + regRefSpec, exists := registries[d.Registry] + if !exists { + return fmt.Errorf("registry '%s' does not exist", d.Registry) + } + + r, err := Locate(a, regRefSpec) + if err != nil { + return err + } + + vendorPath := filepath.Join(a.Root(), "vendor") + + // Get all directories and files first, then write to disk. This + // protects us from failing with a half-cached dependency because of + // a network failure. + directories := []string{} + files := map[string][]byte{} + _, libRef, err := r.ResolveLibrary( + d.Part, + customName, + d.Version, + func(relPath string, contents []byte) error { + files[filepath.Join(vendorPath, relPath)] = contents + return nil + }, + func(relPath string) error { + directories = append(directories, filepath.Join(vendorPath, relPath)) + return nil + }) + if err != nil { + return err + } + + // Add library to app specification, but wait to write it out until + // the end, in case one of the network calls fails. + + log.Infof("Retrieved %d files", len(files)) + + for _, dir := range directories { + if err = a.Fs().MkdirAll(dir, app.DefaultFolderPermissions); err != nil { + return err + } + } + + for path, content := range files { + if err = afero.WriteFile(a.Fs(), path, content, app.DefaultFilePermissions); err != nil { + return err + } + } + + return a.UpdateLib(customName, libRef) +} diff --git a/pkg/registry/fs.go b/pkg/registry/fs.go new file mode 100644 index 0000000000000000000000000000000000000000..0f341881ad9fa163d912ddc9638481c203e04cf7 --- /dev/null +++ b/pkg/registry/fs.go @@ -0,0 +1,152 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 registry + +import ( + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/ksonnet/ksonnet/pkg/parts" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +// Fs is a registry based on a local filesystem. +type Fs struct { + app app.App + spec *app.RegistryRefSpec + root string +} + +// NewFs creates an instance of Fs. Assign a name to the RegistryRefSpec if you want +// the Fs to know it's name. +func NewFs(a app.App, registryRef *app.RegistryRefSpec) (*Fs, error) { + fs := &Fs{ + app: a, + spec: registryRef, + } + + u, err := url.Parse(registryRef.URI) + if err != nil { + return nil, err + } + + if u.Scheme != "file" { + return nil, errors.Errorf("unknown file protocol %q", u.Scheme) + } + + fs.root = u.Path + + return fs, nil +} + +var _ Registry = (*Fs)(nil) + +// Name is the registry name. +func (fs *Fs) Name() string { + return fs.spec.Name +} + +// Protocol is the registry protocol. +func (fs *Fs) Protocol() string { + return fs.spec.Protocol +} + +// URI is the registry URI. +func (fs *Fs) URI() string { + return fs.spec.URI +} + +// RegistrySpecDir is the registry directory. +func (fs *Fs) RegistrySpecDir() string { + return fs.root +} + +// RegistrySpecFilePath is the path for the registry.yaml +func (fs *Fs) RegistrySpecFilePath() string { + return filepath.Join(fs.root, registryYAMLFile) +} + +// FetchRegistrySpec fetches the registry spec. +func (fs *Fs) FetchRegistrySpec() (*Spec, error) { + logrus.Debugf("fs: fetching fs registry spec %s", fs.RegistrySpecFilePath()) + data, err := afero.ReadFile(fs.app.Fs(), fs.RegistrySpecFilePath()) + if err != nil { + return nil, err + } + + return Unmarshal(data) +} + +// MakeRegistryRefSpec returns an app registry ref spec. +func (fs *Fs) MakeRegistryRefSpec() *app.RegistryRefSpec { + return fs.spec +} + +// ResolveLibrarySpec returns a resolved spec for a part. `libRefSpec` is ignored. +func (fs *Fs) ResolveLibrarySpec(partName, libRefSpec string) (*parts.Spec, error) { + partRoot := filepath.Join(fs.RegistrySpecDir(), partName, partsYAMLFile) + data, err := afero.ReadFile(fs.app.Fs(), partRoot) + if err != nil { + return nil, err + } + + return parts.Unmarshal(data) +} + +// ResolveLibrary fetches the part and creates a parts spec and library ref spec. +func (fs *Fs) ResolveLibrary(partName, partAlias, libRefSpec string, onFile ResolveFile, onDir ResolveDirectory) (*parts.Spec, *app.LibraryRefSpec, error) { + partRoot := filepath.Join(fs.RegistrySpecDir(), partName) + parentDir := filepath.Dir(fs.RegistrySpecDir()) + + partsSpec, err := fs.ResolveLibrarySpec(partName, libRefSpec) + if err != nil { + return nil, nil, err + } + + err = afero.Walk(fs.app.Fs(), partRoot, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + libPath := strings.TrimPrefix(path, parentDir+"/") + if fi.IsDir() { + return onDir(libPath) + } + + data, err := afero.ReadFile(fs.app.Fs(), path) + if err != nil { + return err + } + return onFile(libPath, data) + }) + + if err != nil { + return nil, nil, err + } + + refSpec := &app.LibraryRefSpec{ + Name: partAlias, + Registry: fs.Name(), + } + + return partsSpec, refSpec, nil + +} diff --git a/pkg/registry/fs_test.go b/pkg/registry/fs_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f8eda1e45c816cc24fb3224beb5f23d37b14e8bf --- /dev/null +++ b/pkg/registry/fs_test.go @@ -0,0 +1,250 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 registry + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/ksonnet/ksonnet/metadata/app/mocks" + "github.com/ksonnet/ksonnet/pkg/parts" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func withRFS(t *testing.T, fn func(*Fs, *mocks.App, afero.Fs)) { + fs := afero.NewMemMapFs() + appMock := &mocks.App{} + appMock.On("Fs").Return(fs) + appMock.On("Root").Return("/app") + appMock.On("LibPath", mock.AnythingOfType("string")).Return(filepath.Join("/app", "lib", "v1.8.7"), nil) + + spec := &app.RegistryRefSpec{ + Name: "local", + Protocol: ProtocolFilesystem, + URI: "file:///work/local", + } + + partRoot := filepath.Join("testdata", "part", "incubator") + err := filepath.Walk(partRoot, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + newPath := filepath.Join("/work", "local", strings.TrimPrefix(path, partRoot)) + if fi.IsDir() { + return fs.MkdirAll(newPath, 0750) + } + + data, err := ioutil.ReadFile(path) + require.NoError(t, err) + + return afero.WriteFile(fs, newPath, data, 0644) + }) + require.NoError(t, err) + + data, err := ioutil.ReadFile(filepath.Join("testdata", "fs-registry.yaml")) + require.NoError(t, err) + + err = afero.WriteFile(fs, "/work/local/registry.yaml", data, 0644) + require.NoError(t, err) + + rfs, err := NewFs(appMock, spec) + require.NoError(t, err) + + fn(rfs, appMock, fs) +} + +func TestFs_Name(t *testing.T) { + withRFS(t, func(rfs *Fs, appMock *mocks.App, fs afero.Fs) { + assert.Equal(t, "local", rfs.Name()) + }) +} + +func TestFs_Protocol(t *testing.T) { + withRFS(t, func(rfs *Fs, appMock *mocks.App, fs afero.Fs) { + assert.Equal(t, ProtocolFilesystem, rfs.Protocol()) + }) +} + +func TestFs_URI(t *testing.T) { + withRFS(t, func(rfs *Fs, appMock *mocks.App, fs afero.Fs) { + assert.Equal(t, "file:///work/local", rfs.URI()) + }) +} + +func TestFs_RegistrySpecDir(t *testing.T) { + withRFS(t, func(rfs *Fs, appMock *mocks.App, fs afero.Fs) { + assert.Equal(t, "/work/local", rfs.RegistrySpecDir()) + }) +} + +func TestFs_RegistrySpecFilePath(t *testing.T) { + withRFS(t, func(rfs *Fs, appMock *mocks.App, fs afero.Fs) { + assert.Equal(t, "/work/local/registry.yaml", rfs.RegistrySpecFilePath()) + }) +} + +func TestFs_FetchRegistrySpec(t *testing.T) { + withRFS(t, func(rfs *Fs, appMock *mocks.App, fs afero.Fs) { + spec, err := rfs.FetchRegistrySpec() + require.NoError(t, err) + + expected := &Spec{ + APIVersion: "0.1.0", + Kind: "ksonnet.io/registry", + Libraries: LibraryRefSpecs{ + "apache": &LibraryRef{ + Path: "apache", + }, + }, + } + + assert.Equal(t, expected, spec) + }) +} + +func TestFs_MakeRegistryRefSpec(t *testing.T) { + withRFS(t, func(rfs *Fs, appMock *mocks.App, fs afero.Fs) { + expected := &app.RegistryRefSpec{ + Name: "local", + Protocol: ProtocolFilesystem, + URI: "file:///work/local", + } + assert.Equal(t, expected, rfs.MakeRegistryRefSpec()) + + }) +} + +func TestFs_ResolveLibrarySpec(t *testing.T) { + withRFS(t, func(rfs *Fs, appMock *mocks.App, fs afero.Fs) { + spec, err := rfs.ResolveLibrarySpec("apache", "") + require.NoError(t, err) + + expected := &parts.Spec{ + APIVersion: "0.0.1", + Kind: "ksonnet.io/parts", + Name: "apache", + Description: "part description", + Author: "author", + Contributors: parts.ContributorSpecs{ + &parts.ContributorSpec{Name: "author 1", Email: "email@example.com"}, + &parts.ContributorSpec{Name: "author 2", Email: "email@example.com"}, + }, + Repository: parts.RepositorySpec{ + Type: "git", + URL: "https://github.com/ksonnet/mixins", + }, + Bugs: &parts.BugSpec{ + URL: "https://github.com/ksonnet/mixins/issues", + }, + Keywords: []string{"apache", "server", "http"}, + QuickStart: &parts.QuickStartSpec{ + Prototype: "io.ksonnet.pkg.apache-simple", + ComponentName: "apache", + Flags: map[string]string{ + "name": "apache", + "namespace": "default", + }, + Comment: "Run a simple Apache server", + }, + License: "Apache 2.0", + } + + assert.Equal(t, expected, spec) + + }) +} + +func TestFs_ResolveLibrary(t *testing.T) { + withRFS(t, func(rfs *Fs, appMock *mocks.App, fs afero.Fs) { + var files []string + onFile := func(relPath string, contents []byte) error { + files = append(files, relPath) + return nil + } + + var directories []string + onDir := func(relPath string) error { + directories = append(directories, relPath) + return nil + } + + spec, libRefSpec, err := rfs.ResolveLibrary("apache", "alias", "54321", onFile, onDir) + require.NoError(t, err) + + expectedSpec := &parts.Spec{ + APIVersion: "0.0.1", + Kind: "ksonnet.io/parts", + Name: "apache", + Description: "part description", + Author: "author", + Contributors: parts.ContributorSpecs{ + &parts.ContributorSpec{Name: "author 1", Email: "email@example.com"}, + &parts.ContributorSpec{Name: "author 2", Email: "email@example.com"}, + }, + Repository: parts.RepositorySpec{ + Type: "git", + URL: "https://github.com/ksonnet/mixins", + }, + Bugs: &parts.BugSpec{ + URL: "https://github.com/ksonnet/mixins/issues", + }, + Keywords: []string{"apache", "server", "http"}, + QuickStart: &parts.QuickStartSpec{ + Prototype: "io.ksonnet.pkg.apache-simple", + ComponentName: "apache", + Flags: map[string]string{ + "name": "apache", + "namespace": "default", + }, + Comment: "Run a simple Apache server", + }, + License: "Apache 2.0", + } + assert.Equal(t, expectedSpec, spec) + + expectedLibRefSpec := &app.LibraryRefSpec{ + Name: "alias", + Registry: "local", + } + assert.Equal(t, expectedLibRefSpec, libRefSpec) + + expectedFiles := []string{ + "local/apache/README.md", + "local/apache/apache.libsonnet", + "local/apache/examples/apache.jsonnet", + "local/apache/examples/generated.yaml", + "local/apache/parts.yaml", + "local/apache/prototypes/apache-simple.jsonnet", + } + assert.Equal(t, expectedFiles, files) + + expectedDirs := []string{ + "local/apache", + "local/apache/examples", + "local/apache/prototypes", + } + assert.Equal(t, expectedDirs, directories) + + }) +} diff --git a/pkg/registry/github.go b/pkg/registry/github.go index c37663c86188e8ad440dc9def928d22fe1decfc9..ab96f112173fe3e89fa054d748befaf93cc048c3 100644 --- a/pkg/registry/github.go +++ b/pkg/registry/github.go @@ -15,35 +15,392 @@ package registry -import "github.com/ksonnet/ksonnet/metadata/app" +import ( + "context" + "fmt" + "net/url" + "path" + "strings" -// GitHub is a GitHub based registry. + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/ksonnet/ksonnet/pkg/parts" + "github.com/ksonnet/ksonnet/pkg/util/github" + "github.com/pkg/errors" +) + +const ( + rawGitHubRoot = "https://raw.githubusercontent.com" + defaultGitHubBranch = "master" + + uriField = "uri" + refSpecField = "refSpec" + resolvedSHAField = "resolvedSHA" +) + +var ( + // errInvalidURI is an invalid github uri error. + errInvalidURI = fmt.Errorf("Invalid GitHub URI: try navigating in GitHub to the URI of the folder containing the 'yaml', and using that URI instead. Generally, this URI should be of the form 'github.com/{organization}/{repository}/tree/{branch}/[path-to-directory]'") + + githubFactory = func(spec *app.RegistryRefSpec) (*GitHub, error) { + return NewGitHub(spec) + } +) + +// GitHubClient is an option for the setting a github client. +func GitHubClient(c github.GitHub) GitHubOpt { + return func(gh *GitHub) { + gh.ghClient = c + } +} + +// GitHubOpt is an option for configuring GitHub. +type GitHubOpt func(*GitHub) + +// GitHub is a Github Registry type GitHub struct { - name string - spec *app.RegistryRefSpec + name string + hd *hubDescriptor + ghClient github.GitHub + spec *app.RegistryRefSpec } // NewGitHub creates an instance of GitHub. -func NewGitHub(name string, spec *app.RegistryRefSpec) *GitHub { - return &GitHub{ - name: name, - spec: spec, +func NewGitHub(registryRef *app.RegistryRefSpec, opts ...GitHubOpt) (*GitHub, error) { + if registryRef == nil { + return nil, errors.New("registry ref is nil") } -} -var _ Registry = (*GitHub)(nil) + gh := &GitHub{ + name: registryRef.Name, + spec: registryRef, + ghClient: github.DefaultClient, + } + + hd, err := parseGitHubURI(gh.URI()) + if err != nil { + return nil, err + } + gh.hd = hd + + for _, opt := range opts { + opt(gh) + } + + ctx := context.Background() + sha, err := gh.ghClient.CommitSHA1(ctx, hd.Repo(), hd.refSpec) + if err != nil { + return nil, errors.Wrap(err, "unable to find SHA1 for repo") + } + + gh.spec.GitVersion = &app.GitVersionSpec{ + RefSpec: hd.refSpec, + CommitSHA: sha, + } + + return gh, nil +} // Name is the registry name. -func (g *GitHub) Name() string { - return g.name +func (gh *GitHub) Name() string { + return gh.name } // Protocol is the registry protocol. -func (g *GitHub) Protocol() string { - return g.spec.Protocol +func (gh *GitHub) Protocol() string { + return gh.spec.Protocol } // URI is the registry URI. -func (g *GitHub) URI() string { - return g.spec.URI +func (gh *GitHub) URI() string { + return gh.spec.URI +} + +// RegistrySpecDir is the registry directory. +func (gh *GitHub) RegistrySpecDir() string { + return gh.Name() +} + +// RegistrySpecFilePath is the path for the registry.yaml +func (gh *GitHub) RegistrySpecFilePath() string { + if gh.spec.GitVersion.CommitSHA != "" { + return path.Join(gh.Name(), gh.spec.GitVersion.CommitSHA+".yaml") + } + return path.Join(gh.Name(), gh.spec.GitVersion.RefSpec+".yaml") +} + +// FetchRegistrySpec fetches the registry spec. +func (gh *GitHub) FetchRegistrySpec() (*Spec, error) { + ctx := context.Background() + + file, _, err := gh.ghClient.Contents(ctx, gh.hd.Repo(), gh.hd.regSpecRepoPath, + gh.spec.GitVersion.CommitSHA) + if file == nil { + return nil, fmt.Errorf("Could not find valid registry at uri '%s/%s/%s' and refspec '%s' (resolves to sha '%s')", + gh.hd.org, gh.hd.org, gh.hd.regSpecRepoPath, gh.spec.GitVersion.RefSpec, + gh.spec.GitVersion.CommitSHA) + } else if err != nil { + return nil, err + } + + registrySpecText, err := file.GetContent() + if err != nil { + return nil, err + } + + // Deserialize, return. + registrySpec, err := Unmarshal([]byte(registrySpecText)) + if err != nil { + return nil, err + } + + registrySpec.GitVersion = &app.GitVersionSpec{ + RefSpec: gh.spec.GitVersion.RefSpec, + CommitSHA: gh.spec.GitVersion.CommitSHA, + } + + return registrySpec, nil +} + +// MakeRegistryRefSpec returns an app registry ref spec. +func (gh *GitHub) MakeRegistryRefSpec() *app.RegistryRefSpec { + return gh.spec +} + +// ResolveLibrarySpec returns a resolved spec for a part. +func (gh *GitHub) ResolveLibrarySpec(partName, libRefSpec string) (*parts.Spec, error) { + ctx := context.Background() + resolvedSHA, err := gh.ghClient.CommitSHA1(ctx, gh.hd.Repo(), libRefSpec) + if err != nil { + return nil, err + } + + // Resolve app spec. + appSpecPath := strings.Join([]string{gh.hd.regRepoPath, partName, partsYAMLFile}, "/") + + file, directory, err := gh.ghClient.Contents(ctx, gh.hd.Repo(), appSpecPath, resolvedSHA) + if err != nil { + return nil, err + } else if directory != nil { + return nil, fmt.Errorf("Can't download library specification; resource '%s' points at a file", gh.registrySpecRawURL()) + } + + partsSpecText, err := file.GetContent() + if err != nil { + return nil, err + } + + parts, err := parts.Unmarshal([]byte(partsSpecText)) + if err != nil { + return nil, err + } + + return parts, nil +} + +// ResolveLibrary fetches the part and creates a parts spec and library ref spec. +func (gh *GitHub) ResolveLibrary(partName, partAlias, libRefSpec string, onFile ResolveFile, onDir ResolveDirectory) (*parts.Spec, *app.LibraryRefSpec, error) { + defaultRefSpec := "master" + + // Resolve `version` (a git refspec) to a specific SHA. + ctx := context.Background() + if len(libRefSpec) == 0 { + libRefSpec = defaultRefSpec + } + + resolvedSHA, err := gh.ghClient.CommitSHA1(ctx, gh.hd.Repo(), libRefSpec) + if err != nil { + return nil, nil, err + } + + // Resolve directories and files. + path := strings.Join([]string{gh.hd.regRepoPath, partName}, "/") + err = gh.resolveDir(partName, path, resolvedSHA, onFile, onDir) + if err != nil { + return nil, nil, err + } + + // Resolve app spec. + appSpecPath := strings.Join([]string{path, partsYAMLFile}, "/") + ctx = context.Background() + file, directory, err := gh.ghClient.Contents(ctx, gh.hd.Repo(), appSpecPath, resolvedSHA) + + if err != nil { + return nil, nil, err + } else if directory != nil { + return nil, nil, fmt.Errorf("Can't download library specification; resource '%s' points at a file", gh.registrySpecRawURL()) + } + + partsSpecText, err := file.GetContent() + if err != nil { + return nil, nil, err + } + + parts, err := parts.Unmarshal([]byte(partsSpecText)) + if err != nil { + return nil, nil, err + } + + refSpec := app.LibraryRefSpec{ + Name: partAlias, + Registry: gh.Name(), + GitVersion: &app.GitVersionSpec{ + RefSpec: libRefSpec, + CommitSHA: resolvedSHA, + }, + } + + return parts, &refSpec, nil +} + +func (gh *GitHub) resolveDir(libID, path, version string, onFile ResolveFile, onDir ResolveDirectory) error { + ctx := context.Background() + + file, directory, err := gh.ghClient.Contents(ctx, gh.hd.Repo(), path, version) + if err != nil { + return err + } else if file != nil { + return fmt.Errorf("Lib ID %q resolves to a file in registry %q", libID, gh.Name()) + } + + for _, item := range directory { + switch item.GetType() { + case "file": + itemPath := item.GetPath() + file, directory, err := gh.ghClient.Contents(ctx, gh.hd.Repo(), itemPath, version) + if err != nil { + return err + } else if directory != nil { + return fmt.Errorf("INTERNAL ERROR: GitHub API reported resource %q of type file, but returned type dir", itemPath) + } + contents, err := file.GetContent() + if err != nil { + return err + } + if err := onFile(itemPath, []byte(contents)); err != nil { + return err + } + case "dir": + itemPath := item.GetPath() + if err := onDir(itemPath); err != nil { + return err + } + if err := gh.resolveDir(libID, itemPath, version, onFile, onDir); err != nil { + return err + } + case "symlink": + case "submodule": + return fmt.Errorf("Invalid library %q; ksonnet doesn't support libraries with symlinks or submodules", libID) + } + } + + return nil +} + +func (gh *GitHub) registrySpecRawURL() string { + return strings.Join([]string{ + rawGitHubRoot, + gh.hd.org, + gh.hd.repo, + gh.spec.GitVersion.RefSpec, + gh.hd.regSpecRepoPath}, "/") +} + +type hubDescriptor struct { + org string + repo string + refSpec string + regRepoPath string + regSpecRepoPath string +} + +func (hd *hubDescriptor) Repo() github.Repo { + return github.Repo{Org: hd.org, Repo: hd.repo} +} + +// func parseGitHubURI(uri string) (org, repo, refSpec, regRepoPath, regSpecRepoPath string, err error) { +func parseGitHubURI(uri string) (hd *hubDescriptor, err error) { + // Normalize URI. + uri = strings.TrimSpace(uri) + if strings.HasPrefix(uri, "http://github.com") || strings.HasPrefix(uri, "https://github.com") || strings.HasPrefix(uri, "http://www.github.com") || strings.HasPrefix(uri, "https://www.github.com") { + // Do nothing. + } else if strings.HasPrefix(uri, "github.com") || strings.HasPrefix(uri, "www.github.com") { + uri = "http://" + uri + } else { + return nil, errors.Errorf("Registries using protocol 'github' must provide URIs beginning with 'github.com' (optionally prefaced with 'http', 'https', 'www', and so on") + } + + parsed, err := url.Parse(uri) + if err != nil { + return nil, err + } + + if len(parsed.Query()) != 0 { + return nil, errors.Errorf("No query strings allowed in registry URI:\n%s", uri) + } + + components := strings.Split(parsed.Path, "/") + if len(components) < 3 { + return nil, errors.Errorf("GitHub URI must point at a repository:\n%s", uri) + } + + hd = &hubDescriptor{} + + // NOTE: The first component is always blank, because the path + // begins like: '/whatever'. + hd.org = components[1] + hd.repo = components[2] + + // + // Parse out `regSpecRepoPath`. There are a few cases: + // * URI points at a directory inside the respoitory, e.g., + // 'http://github.com/ksonnet/parts/tree/master/incubator' + // * URI points at an 'app.yaml', e.g., + // 'http://github.com/ksonnet/parts/blob/master/yaml' + // * URI points at a repository root, e.g., + // 'http://github.com/ksonnet/parts' + // + if len := len(components); len > 4 { + hd.refSpec = components[4] + + // + // Case where we're pointing at either a directory inside a GitHub + // URL, or an 'app.yaml' inside a GitHub URL. + // + + // See note above about first component being blank. + if components[3] == "tree" { + // If we have a trailing '/' character, last component will be blank. Make + // sure that `regRepoPath` does not contain a trailing `/`. + if components[len-1] == "" { + hd.regRepoPath = strings.Join(components[5:len-1], "/") + components[len-1] = registryYAMLFile + } else { + hd.regRepoPath = strings.Join(components[5:], "/") + components = append(components, registryYAMLFile) + } + hd.regSpecRepoPath = strings.Join(components[5:], "/") + return + } else if components[3] == "blob" && components[len-1] == registryYAMLFile { + hd.regRepoPath = strings.Join(components[5:len-1], "/") + // Path to the `yaml` (may or may not exist). + hd.regSpecRepoPath = strings.Join(components[5:], "/") + return + } else { + return nil, errInvalidURI + } + } else { + hd.refSpec = defaultGitHubBranch + + // Else, URI should point at repository root. + if components[len-1] == "" { + components[len-1] = defaultGitHubBranch + components = append(components, registryYAMLFile) + } else { + components = append(components, defaultGitHubBranch, registryYAMLFile) + } + + hd.regRepoPath = "" + hd.regSpecRepoPath = registryYAMLFile + return + } } diff --git a/pkg/registry/github_test.go b/pkg/registry/github_test.go index a69e095fe31f74893f13f88c1cb8e0b11050041d..0701c0d771c8532e87135c9cd1c0ed8190bad342 100644 --- a/pkg/registry/github_test.go +++ b/pkg/registry/github_test.go @@ -16,21 +16,487 @@ package registry import ( + "io/ioutil" + "os" + "path/filepath" + "strings" "testing" + "github.com/google/go-github/github" "github.com/ksonnet/ksonnet/metadata/app" + "github.com/ksonnet/ksonnet/pkg/parts" + ghutil "github.com/ksonnet/ksonnet/pkg/util/github" + "github.com/ksonnet/ksonnet/pkg/util/github/mocks" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) -func TestGithub(t *testing.T) { +func makeGh(t *testing.T, u, sha1 string) (*GitHub, *mocks.GitHub) { + ghMock := &mocks.GitHub{} + ghMock.On("CommitSHA1", mock.Anything, ghutil.Repo{Org: "ksonnet", Repo: "parts"}, "master"). + Return(sha1, nil) + + optGh := GitHubClient(ghMock) + spec := &app.RegistryRefSpec{ - Protocol: "github", - URI: "github.com/foo/bar", + Name: "incubator", + Protocol: ProtocolGitHub, + URI: "github.com/ksonnet/parts/tree/master/incubator", + } + + g, err := NewGitHub(spec, optGh) + require.NoError(t, err) + + return g, ghMock +} + +func buildContent(t *testing.T, name string) *github.RepositoryContent { + path := name + if !strings.HasPrefix(name, "testdata") { + path = filepath.Join("testdata", name) + } + data, err := ioutil.ReadFile(path) + require.NoError(t, err) + + path = strings.TrimPrefix(path, "testdata/part/") + + rc := &github.RepositoryContent{ + Type: github.String("file"), + Content: github.String(string(data)), + Path: github.String(path), + } + + return rc +} + +func buildContentDir(t *testing.T, name string) []*github.RepositoryContent { + path := name + if !strings.HasPrefix(name, "testdata") { + path = filepath.Join("testdata", name) } + fi, err := os.Stat(path) + require.NoError(t, err) + require.True(t, fi.IsDir()) - g := NewGitHub("incubator", spec) + rcs := make([]*github.RepositoryContent, 0) + + fis, err := ioutil.ReadDir(path) + require.NoError(t, err) + + for _, fi = range fis { + childPath := filepath.Join(strings.TrimPrefix(path, "testdata/"), fi.Name()) + + if fi.IsDir() { + rc := &github.RepositoryContent{ + Type: github.String("dir"), + Path: github.String(strings.TrimPrefix(childPath, "part/")), + } + rcs = append(rcs, rc) + continue + } + rc := buildContent(t, childPath) + rcs = append(rcs, rc) + } + + return rcs +} + +func TestGithub_Name(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, _ := makeGh(t, u, "12345") assert.Equal(t, "incubator", g.Name()) - assert.Equal(t, "github", g.Protocol()) - assert.Equal(t, "github.com/foo/bar", g.URI()) +} + +func TestGithub_Protocol(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, _ := makeGh(t, u, "12345") + + assert.Equal(t, ProtocolGitHub, g.Protocol()) +} + +func TestGithub_URI(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, _ := makeGh(t, u, "12345") + + assert.Equal(t, u, g.URI()) +} + +func TestGithub_RegistrySpecDir(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, _ := makeGh(t, u, "12345") + + assert.Equal(t, "incubator", g.RegistrySpecDir()) +} + +func TestGithub_RegistrySpecFilePath(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, _ := makeGh(t, u, "12345") + + assert.Equal(t, "incubator/12345.yaml", g.RegistrySpecFilePath()) +} + +func TestGithub_RegistrySpecFilePath_no_sha1(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, _ := makeGh(t, u, "") + + assert.Equal(t, "incubator/master.yaml", g.RegistrySpecFilePath()) +} + +func TestGithub_FetchRegistrySpec(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, ghMock := makeGh(t, u, "12345") + + file := buildContent(t, "registry.yaml") + + ghMock.On( + "Contents", + mock.Anything, + ghutil.Repo{Org: "ksonnet", Repo: "parts"}, + "incubator/registry.yaml", + "12345", + ).Return(file, nil, nil) + + spec, err := g.FetchRegistrySpec() + require.NoError(t, err) + + expected := &Spec{ + APIVersion: "0.1.0", + Kind: "ksonnet.io/registry", + GitVersion: &app.GitVersionSpec{ + CommitSHA: "12345", + RefSpec: "master", + }, + Libraries: LibraryRefSpecs{ + "apache": &LibraryRef{ + Path: "apache", + Version: "master", + }, + }, + } + + assert.Equal(t, expected, spec) +} + +func TestGithub_FetchRegistrySpec_invalid_manifest(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, ghMock := makeGh(t, u, "12345") + + file := buildContent(t, "invalid-registry.yaml") + + ghMock.On( + "Contents", + mock.Anything, + ghutil.Repo{Org: "ksonnet", Repo: "parts"}, + "incubator/registry.yaml", + "12345", + ).Return(file, nil, nil) + + _, err := g.FetchRegistrySpec() + require.Error(t, err) +} + +func TestGithub_MakeRegistryRefSpec(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, _ := makeGh(t, u, "12345") + + expected := &app.RegistryRefSpec{ + Name: "incubator", + Protocol: ProtocolGitHub, + URI: "github.com/ksonnet/parts/tree/master/incubator", + GitVersion: &app.GitVersionSpec{ + CommitSHA: "12345", + RefSpec: "master", + }, + } + + assert.Equal(t, expected, g.MakeRegistryRefSpec()) +} + +func TestGithub_ResolveLibrarySpec(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, ghMock := makeGh(t, u, "12345") + + repo := ghutil.Repo{Org: "ksonnet", Repo: "parts"} + + ghMock.On("CommitSHA1", mock.Anything, repo, "54321").Return("54321", nil) + + file := buildContent(t, "apache-part.yaml") + + ghMock.On("Contents", mock.Anything, repo, "incubator/apache/parts.yaml", "54321"). + Return(file, nil, nil) + + spec, err := g.ResolveLibrarySpec("apache", "54321") + require.NoError(t, err) + + expected := &parts.Spec{ + APIVersion: "0.0.1", + Kind: "ksonnet.io/parts", + Name: "apache", + Description: "part description", + Author: "author", + Contributors: parts.ContributorSpecs{ + &parts.ContributorSpec{Name: "author 1", Email: "email@example.com"}, + &parts.ContributorSpec{Name: "author 2", Email: "email@example.com"}, + }, + Repository: parts.RepositorySpec{ + Type: "git", + URL: "https://github.com/ksonnet/mixins", + }, + Bugs: &parts.BugSpec{ + URL: "https://github.com/ksonnet/mixins/issues", + }, + Keywords: []string{"apache", "server", "http"}, + QuickStart: &parts.QuickStartSpec{ + Prototype: "io.ksonnet.pkg.apache-simple", + ComponentName: "apache", + Flags: map[string]string{ + "name": "apache", + "namespace": "default", + }, + Comment: "Run a simple Apache server", + }, + License: "Apache 2.0", + } + + assert.Equal(t, expected, spec) +} + +func mockPartFs(t *testing.T, repo ghutil.Repo, ghMock *mocks.GitHub, name, sha1 string) { + root := filepath.Join("testdata", "part", name) + _, err := os.Stat(root) + require.NoError(t, err) + + err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { + require.NoError(t, err) + + if fi.IsDir() { + rcs := buildContentDir(t, path) + path = strings.TrimPrefix(path, filepath.Join("testdata", "part")) + path = strings.TrimPrefix(path, "/") + + ghMock.On("Contents", mock.Anything, repo, path, sha1).Return(nil, rcs, nil) + return nil + } + + rc := buildContent(t, path) + path = strings.TrimPrefix(path, "testdata/part/") + ghMock.On("Contents", mock.Anything, repo, path, sha1).Return(rc, nil, nil) + return nil + }) + + require.NoError(t, err) +} + +func TestGithub_ResolveLibrary(t *testing.T) { + u := "github.com/ksonnet/parts/tree/master/incubator" + g, ghMock := makeGh(t, u, "12345") + + repo := ghutil.Repo{Org: "ksonnet", Repo: "parts"} + + ghMock.On("CommitSHA1", mock.Anything, repo, "54321").Return("54321", nil) + + partName := filepath.Join("incubator", "apache") + mockPartFs(t, repo, ghMock, partName, "54321") + + var files []string + onFile := func(relPath string, contents []byte) error { + files = append(files, relPath) + return nil + } + + var directories []string + onDir := func(relPath string) error { + directories = append(directories, relPath) + return nil + } + + spec, libRefSpec, err := g.ResolveLibrary("apache", "alias", "54321", onFile, onDir) + require.NoError(t, err) + + expectedSpec := &parts.Spec{ + APIVersion: "0.0.1", + Kind: "ksonnet.io/parts", + Name: "apache", + Description: "part description", + Author: "author", + Contributors: parts.ContributorSpecs{ + &parts.ContributorSpec{Name: "author 1", Email: "email@example.com"}, + &parts.ContributorSpec{Name: "author 2", Email: "email@example.com"}, + }, + Repository: parts.RepositorySpec{ + Type: "git", + URL: "https://github.com/ksonnet/mixins", + }, + Bugs: &parts.BugSpec{ + URL: "https://github.com/ksonnet/mixins/issues", + }, + Keywords: []string{"apache", "server", "http"}, + QuickStart: &parts.QuickStartSpec{ + Prototype: "io.ksonnet.pkg.apache-simple", + ComponentName: "apache", + Flags: map[string]string{ + "name": "apache", + "namespace": "default", + }, + Comment: "Run a simple Apache server", + }, + License: "Apache 2.0", + } + assert.Equal(t, expectedSpec, spec) + + expectedLibRefSpec := &app.LibraryRefSpec{ + Name: "alias", + Registry: "incubator", + GitVersion: &app.GitVersionSpec{ + RefSpec: "54321", + CommitSHA: "54321", + }, + } + assert.Equal(t, expectedLibRefSpec, libRefSpec) + + expectedFiles := []string{ + "incubator/apache/README.md", + "incubator/apache/apache.libsonnet", + "incubator/apache/examples/apache.jsonnet", + "incubator/apache/examples/generated.yaml", + "incubator/apache/parts.yaml", + "incubator/apache/prototypes/apache-simple.jsonnet", + } + assert.Equal(t, expectedFiles, files) + + expectedDirs := []string{ + "incubator/apache/examples", + "incubator/apache/prototypes", + } + assert.Equal(t, expectedDirs, directories) +} + +func Test_parseGitHubURI(t *testing.T) { + tests := []struct { + // Specification to parse. + uri string + + // Optional error to check. + targetErr error + + // Optional results to verify. + targetOrg string + targetRepo string + targetRefSpec string + targetRegistryRepoPath string + targetRegistrySpecRepoPath string + }{ + // + // `parseGitHubURI` should correctly parse org, repo, and refspec. Does not + // test path parsing. + // + { + uri: "github.com/exampleOrg1/exampleRepo1", + + targetOrg: "exampleOrg1", + targetRepo: "exampleRepo1", + targetRefSpec: "master", + targetRegistryRepoPath: "", + targetRegistrySpecRepoPath: "registry.yaml", + }, + { + uri: "github.com/exampleOrg2/exampleRepo2/tree/master", + + targetOrg: "exampleOrg2", + targetRepo: "exampleRepo2", + targetRefSpec: "master", + targetRegistryRepoPath: "", + targetRegistrySpecRepoPath: "registry.yaml", + }, + { + uri: "github.com/exampleOrg3/exampleRepo3/tree/exampleBranch1", + + targetOrg: "exampleOrg3", + targetRepo: "exampleRepo3", + targetRefSpec: "exampleBranch1", + targetRegistryRepoPath: "", + targetRegistrySpecRepoPath: "registry.yaml", + }, + { + // Fails because `blob` refers to a file, but this refers to a directory. + uri: "github.com/exampleOrg4/exampleRepo4/blob/master", + targetErr: errInvalidURI, + }, + { + uri: "github.com/exampleOrg4/exampleRepo4/tree/exampleBranch2", + + targetOrg: "exampleOrg4", + targetRepo: "exampleRepo4", + targetRefSpec: "exampleBranch2", + targetRegistryRepoPath: "", + targetRegistrySpecRepoPath: "registry.yaml", + }, + + // + // Parsing URIs with paths. + // + { + // Fails because referring to a directory requires a URI with + // `tree/{branchName}` prepending the path. + uri: "github.com/exampleOrg6/exampleRepo6/path/to/some/registry", + targetErr: errInvalidURI, + }, + { + uri: "github.com/exampleOrg5/exampleRepo5/tree/master/path/to/some/registry", + + targetOrg: "exampleOrg5", + targetRepo: "exampleRepo5", + targetRefSpec: "master", + targetRegistryRepoPath: "path/to/some/registry", + targetRegistrySpecRepoPath: "path/to/some/registry/registry.yaml", + }, + { + uri: "github.com/exampleOrg6/exampleRepo6/tree/exampleBranch3/path/to/some/registry", + + targetOrg: "exampleOrg6", + targetRepo: "exampleRepo6", + targetRefSpec: "exampleBranch3", + targetRegistryRepoPath: "path/to/some/registry", + targetRegistrySpecRepoPath: "path/to/some/registry/registry.yaml", + }, + { + // Fails because `blob` refers to a file, but this refers to a directory. + uri: "github.com/exampleOrg7/exampleRepo7/blob/master", + targetErr: errInvalidURI, + }, + { + // Fails because `blob` refers to a file, but this refers to a directory. + uri: "github.com/exampleOrg5/exampleRepo5/blob/exampleBranch2", + targetErr: errInvalidURI, + }, + } + + prefixes := []string{"http://", "https://", "http://www.", "https://www.", "www.", ""} + suffixes := []string{"/", ""} + + for _, test := range tests { + for _, prefix := range prefixes { + for _, suffix := range suffixes { + uri := prefix + test.uri + suffix + + t.Run(uri, func(t *testing.T) { + hd, err := parseGitHubURI(uri) + if test.targetErr != nil { + require.Equal(t, test.targetErr, err) + return + } + + require.NoError(t, err) + + assert.Equal(t, test.targetOrg, hd.org) + assert.Equal(t, test.targetRepo, hd.repo) + assert.Equal(t, test.targetRefSpec, hd.refSpec) + assert.Equal(t, test.targetRegistryRepoPath, hd.regRepoPath) + assert.Equal(t, test.targetRegistrySpecRepoPath, hd.regSpecRepoPath) + }) + } + } + } } diff --git a/pkg/registry/manager.go b/pkg/registry/manager.go index 7c62ccf1f66b178e0df848f0df658690e2fa54e8..2cd8e272cfcb4067bca6d15afc11a1d9e7c39ccd 100644 --- a/pkg/registry/manager.go +++ b/pkg/registry/manager.go @@ -15,37 +15,75 @@ package registry -import "github.com/ksonnet/ksonnet/metadata/app" +import ( + "path/filepath" + + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/pkg/errors" +) var ( // DefaultManager is the default manager for registries. DefaultManager = &defaultManager{} ) -// Registry is a registry. -type Registry interface { - Name() string - Protocol() string - URI() string +// Locate locates a registry given a spec. +func Locate(a app.App, spec *app.RegistryRefSpec) (Registry, error) { + switch spec.Protocol { + case ProtocolGitHub: + return NewGitHub(spec) + case ProtocolFilesystem: + return NewFs(a, spec) + default: + return nil, errors.Errorf("invalid registry protocol %q", spec.Protocol) + } +} + +// TODO: add this to App +func root(a app.App) string { + return filepath.Join(a.Root(), ".ksonnet", "registries") +} + +func makePath(a app.App, r Registry) string { + path := r.RegistrySpecFilePath() + if filepath.IsAbs(path) { + return path + } + + return filepath.Join(root(a), path) } // Manager is a manager for registry related actions. type Manager interface { + Add(a app.App, name, protoocol, uri, version string) (*Spec, error) // Registries returns a list of alphabetically sorted registries. The // registries are sorted by name. - Registries(ksApp app.App) ([]Registry, error) + List(ksApp app.App) ([]Registry, error) } type defaultManager struct{} var _ Manager = (*defaultManager)(nil) -func (dm *defaultManager) Registries(ksApp app.App) ([]Registry, error) { +func (dm *defaultManager) List(ksApp app.App) ([]Registry, error) { var registries []Registry - for name, regRef := range ksApp.Registries() { - registries = append(registries, NewGitHub(name, regRef)) + appRegistries, err := ksApp.Registries() + if err != nil { + return nil, err + } + for name, regRef := range appRegistries { + regRef.Name = name + r, err := Locate(ksApp, regRef) + if err != nil { + return nil, err + } + registries = append(registries, r) } return registries, nil } + +func (dm *defaultManager) Add(a app.App, name, protoocol, uri, version string) (*Spec, error) { + return Add(a, name, protoocol, uri, version) +} diff --git a/pkg/registry/manager_test.go b/pkg/registry/manager_test.go index 34bcee69d7c5098e039473ebd5b9f705d7ea9739..655f5d7db91a695b5a32b2af004676e866914d17 100644 --- a/pkg/registry/manager_test.go +++ b/pkg/registry/manager_test.go @@ -20,30 +20,24 @@ import ( "github.com/ksonnet/ksonnet/metadata/app" "github.com/ksonnet/ksonnet/metadata/app/mocks" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func Test_defaultManager_Registries(t *testing.T) { +func OffTest_defaultManager_Registries(t *testing.T) { dm := &defaultManager{} specs := app.RegistryRefSpecs{ "incubator": &app.RegistryRefSpec{ - Protocol: "github", - URI: "github.com/foo/bar", + Protocol: ProtocolGitHub, + URI: "github.com/ksonnet/parts/tree/master/incubator", }, } appMock := &mocks.App{} appMock.On("Registries").Return(specs) - registries, err := dm.Registries(appMock) + registries, err := dm.List(appMock) require.NoError(t, err) require.Len(t, registries, 1) - - r := registries[0] - assert.Equal(t, "incubator", r.Name()) - assert.Equal(t, "github", r.Protocol()) - assert.Equal(t, "github.com/foo/bar", r.URI()) } diff --git a/pkg/registry/mocks/Manager.go b/pkg/registry/mocks/Manager.go index ac000ef33f7def4dd048cfc39b580abd7d8b17ce..70a5d42dd7a2e67caa32914092c885d2315fdfd8 100644 --- a/pkg/registry/mocks/Manager.go +++ b/pkg/registry/mocks/Manager.go @@ -25,8 +25,31 @@ type Manager struct { mock.Mock } -// Registries provides a mock function with given fields: ksApp -func (_m *Manager) Registries(ksApp app.App) ([]registry.Registry, error) { +// Add provides a mock function with given fields: a, name, protoocol, uri, version +func (_m *Manager) Add(a app.App, name string, protoocol string, uri string, version string) (*registry.Spec, error) { + ret := _m.Called(a, name, protoocol, uri, version) + + var r0 *registry.Spec + if rf, ok := ret.Get(0).(func(app.App, string, string, string, string) *registry.Spec); ok { + r0 = rf(a, name, protoocol, uri, version) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*registry.Spec) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(app.App, string, string, string, string) error); ok { + r1 = rf(a, name, protoocol, uri, version) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ksApp +func (_m *Manager) List(ksApp app.App) ([]registry.Registry, error) { ret := _m.Called(ksApp) var r0 []registry.Registry diff --git a/pkg/registry/mocks/Registry.go b/pkg/registry/mocks/Registry.go index 34f3ae4529891244738c0881e6be4bb06f7c0a06..9685d0490d0ec31e434d19d2fb2f192b26e4a964 100644 --- a/pkg/registry/mocks/Registry.go +++ b/pkg/registry/mocks/Registry.go @@ -16,13 +16,55 @@ // Code generated by mockery v1.0.0 package mocks +import app "github.com/ksonnet/ksonnet/metadata/app" import mock "github.com/stretchr/testify/mock" +import parts "github.com/ksonnet/ksonnet/pkg/parts" +import registry "github.com/ksonnet/ksonnet/pkg/registry" // Registry is an autogenerated mock type for the Registry type type Registry struct { mock.Mock } +// FetchRegistrySpec provides a mock function with given fields: +func (_m *Registry) FetchRegistrySpec() (*registry.Spec, error) { + ret := _m.Called() + + var r0 *registry.Spec + if rf, ok := ret.Get(0).(func() *registry.Spec); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*registry.Spec) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MakeRegistryRefSpec provides a mock function with given fields: +func (_m *Registry) MakeRegistryRefSpec() *app.RegistryRefSpec { + ret := _m.Called() + + var r0 *app.RegistryRefSpec + if rf, ok := ret.Get(0).(func() *app.RegistryRefSpec); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*app.RegistryRefSpec) + } + } + + return r0 +} + // Name provides a mock function with given fields: func (_m *Registry) Name() string { ret := _m.Called() @@ -51,6 +93,89 @@ func (_m *Registry) Protocol() string { return r0 } +// RegistrySpecDir provides a mock function with given fields: +func (_m *Registry) RegistrySpecDir() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// RegistrySpecFilePath provides a mock function with given fields: +func (_m *Registry) RegistrySpecFilePath() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// ResolveLibrary provides a mock function with given fields: libID, libAlias, version, onFile, onDir +func (_m *Registry) ResolveLibrary(libID string, libAlias string, version string, onFile registry.ResolveFile, onDir registry.ResolveDirectory) (*parts.Spec, *app.LibraryRefSpec, error) { + ret := _m.Called(libID, libAlias, version, onFile, onDir) + + var r0 *parts.Spec + if rf, ok := ret.Get(0).(func(string, string, string, registry.ResolveFile, registry.ResolveDirectory) *parts.Spec); ok { + r0 = rf(libID, libAlias, version, onFile, onDir) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*parts.Spec) + } + } + + var r1 *app.LibraryRefSpec + if rf, ok := ret.Get(1).(func(string, string, string, registry.ResolveFile, registry.ResolveDirectory) *app.LibraryRefSpec); ok { + r1 = rf(libID, libAlias, version, onFile, onDir) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*app.LibraryRefSpec) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(string, string, string, registry.ResolveFile, registry.ResolveDirectory) error); ok { + r2 = rf(libID, libAlias, version, onFile, onDir) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ResolveLibrarySpec provides a mock function with given fields: libID, libRefSpec +func (_m *Registry) ResolveLibrarySpec(libID string, libRefSpec string) (*parts.Spec, error) { + ret := _m.Called(libID, libRefSpec) + + var r0 *parts.Spec + if rf, ok := ret.Get(0).(func(string, string) *parts.Spec); ok { + r0 = rf(libID, libRefSpec) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*parts.Spec) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(libID, libRefSpec) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // URI provides a mock function with given fields: func (_m *Registry) URI() string { ret := _m.Called() diff --git a/metadata/registry/interface.go b/pkg/registry/registry.go similarity index 65% rename from metadata/registry/interface.go rename to pkg/registry/registry.go index a379fda928553ef5ad9214bf0fe9d1c7d6df5ba5..f19aa8ca5a617c47571abbcffedc9ce842c285d8 100644 --- a/metadata/registry/interface.go +++ b/pkg/registry/registry.go @@ -1,4 +1,4 @@ -// Copyright 2018 The kubecfg authors +// Copyright 2018 The ksonnet authors // // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,34 @@ package registry import ( "github.com/ksonnet/ksonnet/metadata/app" - "github.com/ksonnet/ksonnet/metadata/parts" + "github.com/ksonnet/ksonnet/pkg/parts" ) +const ( + // ProtocolFilesystem is the protocol for file system based registries. + ProtocolFilesystem = "fs" + // ProtocolGitHub is a the protocol for GitHub based registries. + ProtocolGitHub = "github" + + registryYAMLFile = "registry.yaml" + partsYAMLFile = "parts.yaml" +) + +// ResolveFile resolves files found when searching a part. type ResolveFile func(relPath string, contents []byte) error + +// ResolveDirectory resolves directories when searching a part. type ResolveDirectory func(relPath string) error -type Manager interface { +// Registry is a Registry +type Registry interface { RegistrySpecDir() string RegistrySpecFilePath() string FetchRegistrySpec() (*Spec, error) MakeRegistryRefSpec() *app.RegistryRefSpec ResolveLibrarySpec(libID, libRefSpec string) (*parts.Spec, error) ResolveLibrary(libID, libAlias, version string, onFile ResolveFile, onDir ResolveDirectory) (*parts.Spec, *app.LibraryRefSpec, error) + Name() string + Protocol() string + URI() string } diff --git a/metadata/registry/schema.go b/pkg/registry/schema.go similarity index 84% rename from metadata/registry/schema.go rename to pkg/registry/schema.go index 5c91de3b6a4fdf130e40ccb3b09f683904b26c4f..9d5f62350860d7dc21b24127d1a06642ab89fda6 100644 --- a/metadata/registry/schema.go +++ b/pkg/registry/schema.go @@ -25,10 +25,13 @@ import ( ) const ( + // DefaultAPIVersion is the default version of the registry API. DefaultAPIVersion = "0.1.0" - DefaultKind = "ksonnet.io/registry" + // DefaultKind is the default kind of the registry API. + DefaultKind = "ksonnet.io/registry" ) +// Spec describes how a registry is stored. type Spec struct { APIVersion string `json:"apiVersion"` Kind string `json:"kind"` @@ -36,6 +39,7 @@ type Spec struct { Libraries LibraryRefSpecs `json:"libraries"` } +// Unmarshal unmarshals bytes to a Spec. func Unmarshal(bytes []byte) (*Spec, error) { schema := Spec{} err := yaml.Unmarshal(bytes, &schema) @@ -50,6 +54,7 @@ func Unmarshal(bytes []byte) (*Spec, error) { return &schema, nil } +// Marshal marshals a Spec to YAML. func (s *Spec) Marshal() ([]byte, error) { return yaml.Marshal(s) } @@ -75,11 +80,14 @@ func (s *Spec) validate() error { return nil } +// Specs is a slice of *Spec. type Specs []*Spec +// LibraryRef is library reference. type LibraryRef struct { Version string `json:"version"` Path string `json:"path"` } +// LibraryRefSpecs maps LibraryRefs to a name. type LibraryRefSpecs map[string]*LibraryRef diff --git a/metadata/registry/schema_test.go b/pkg/registry/schema_test.go similarity index 58% rename from metadata/registry/schema_test.go rename to pkg/registry/schema_test.go index 4ceb2bb6933c4ab32c41967b83e01fe511142217..0ac19b9f2a4b98b98b5cc825188026c27aaff361 100644 --- a/metadata/registry/schema_test.go +++ b/pkg/registry/schema_test.go @@ -16,12 +16,65 @@ package registry import ( + "io/ioutil" "testing" "github.com/blang/semver" + "github.com/ksonnet/ksonnet/metadata/app" + "github.com/stretchr/testify/require" ) -func TestApiVersionValidate(t *testing.T) { +func Test_Unmarshal(t *testing.T) { + data, err := ioutil.ReadFile("testdata/registry.yaml") + require.NoError(t, err) + + spec, err := Unmarshal(data) + require.NoError(t, err) + + expected := &Spec{ + APIVersion: DefaultAPIVersion, + Kind: DefaultKind, + GitVersion: &app.GitVersionSpec{ + CommitSHA: "40285d8a14f1ac5787e405e1023cf0c07f6aa28c", + RefSpec: "master", + }, + Libraries: LibraryRefSpecs{ + "apache": &LibraryRef{ + Path: "apache", + Version: "master", + }, + }, + } + + require.Equal(t, expected, spec) +} + +func TestSpec_Marshal(t *testing.T) { + spec := &Spec{ + APIVersion: DefaultAPIVersion, + Kind: DefaultKind, + GitVersion: &app.GitVersionSpec{ + CommitSHA: "40285d8a14f1ac5787e405e1023cf0c07f6aa28c", + RefSpec: "master", + }, + Libraries: LibraryRefSpecs{ + "apache": &LibraryRef{ + Path: "apache", + Version: "master", + }, + }, + } + + expected, err := ioutil.ReadFile("testdata/registry.yaml") + require.NoError(t, err) + + data, err := spec.Marshal() + require.NoError(t, err) + + require.Equal(t, string(expected), string(data)) +} + +func Test_ApiVersionValidate(t *testing.T) { type spec struct { spec string err bool diff --git a/pkg/registry/testdata/apache-part.yaml b/pkg/registry/testdata/apache-part.yaml new file mode 100644 index 0000000000000000000000000000000000000000..50e38ed13ae8cf2977189480e9dc5852945a0d2a --- /dev/null +++ b/pkg/registry/testdata/apache-part.yaml @@ -0,0 +1,40 @@ +{ + "name": "apache", + "apiVersion": "0.0.1", + "kind": "ksonnet.io/parts", + "description": "part description", + "author": "author", + "contributors": [ + { + "name": "author 1", + "email": "email@example.com" + }, + { + "name": "author 2", + "email": "email@example.com" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/ksonnet/mixins" + }, + "bugs": { + "url": "https://github.com/ksonnet/mixins/issues" + }, + "keywords": [ + "apache", + "server", + "http" + ], + "quickStart": { + "prototype": "io.ksonnet.pkg.apache-simple", + "componentName": "apache", + "flags": { + "name": "apache", + "namespace": "default" + }, + "comment": "Run a simple Apache server" + }, + "license": "Apache 2.0" +} + diff --git a/pkg/registry/testdata/fs-registry.yaml b/pkg/registry/testdata/fs-registry.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e156c872baeaaf5b827efe732129069cd41986c4 --- /dev/null +++ b/pkg/registry/testdata/fs-registry.yaml @@ -0,0 +1,5 @@ +apiVersion: 0.1.0 +kind: ksonnet.io/registry +libraries: + apache: + path: apache diff --git a/pkg/registry/testdata/invalid-registry.yaml b/pkg/registry/testdata/invalid-registry.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/pkg/registry/testdata/part/incubator/apache/README.md b/pkg/registry/testdata/part/incubator/apache/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0efbda17d7f6fb7c77a46afa2471dbe6bf780829 --- /dev/null +++ b/pkg/registry/testdata/part/incubator/apache/README.md @@ -0,0 +1,60 @@ +# apache + +> Apache Ksonnet mixin library contains a simple prototype with pre-configured components to help you deploy a Apache HTTP Server app to a Kubernetes cluster with ease. + +* [Quickstart](#quickstart) +* [Using Prototypes](#using-prototypes) + * [io.ksonnet.pkg.apache-simple](#io.ksonnet.pkg.apache-simple) + +## Quickstart + +*The following commands use the `io.ksonnet.pkg.apache-simple` prototype to generate Kubernetes YAML for apache, and then deploys it to your Kubernetes cluster.* + +First, create a cluster and install the ksonnet CLI (see root-level [README.md](rootReadme)). + +If you haven't yet created a [ksonnet application](linkToSomewhere), do so using `ks init <app-name>`. + +Finally, in the ksonnet application directory, run the following: + +```shell +# Expand prototype as a Jsonnet file, place in a file in the +# `components/` directory. (YAML and JSON are also available.) +$ ks prototype use io.ksonnet.pkg.apache-simple apache \ + --name apache \ + --namespace default + +# Apply to server. +$ ks apply -f apache.jsonnet +``` + +## Using the library + +The library files for apache define a set of relevant *parts* (_e.g._, deployments, services, secrets, and so on) that can be combined to configure apache for a wide variety of scenarios. For example, a database like Redis may need a secret to hold the user password, or it may have no password if it's acting as a cache. + +This library provides a set of pre-fabricated "flavors" (or "distributions") of apache, each of which is configured for a different use case. These are captured as ksonnet *prototypes*, which allow users to interactively customize these distributions for their specific needs. + +These prototypes, as well as how to use them, are enumerated below. + +### io.ksonnet.pkg.apache-simple + +Apache HTTP Server. Apache is deployed using a deployment, and exposed to the network using a service. + +#### Example + +```shell +# Expand prototype as a Jsonnet file, place in a file in the +# `components/` directory. (YAML and JSON are also available.) +$ ks prototype use io.ksonnet.pkg.apache-simple apache \ + --namespace YOUR_NAMESPACE_HERE \ + --name YOUR_NAME_HERE +``` + +#### Parameters + +The available options to pass prototype are: + +* `--namespace=<namespace>`: Namespace to divvy up your cluster; default is 'default' [string] +* `--name=<name>`: Name to identify all Kubernetes objects in this prototype [string] + + +[rootReadme]: https://github.com/ksonnet/mixins diff --git a/pkg/registry/testdata/part/incubator/apache/apache.libsonnet b/pkg/registry/testdata/part/incubator/apache/apache.libsonnet new file mode 100644 index 0000000000000000000000000000000000000000..09e246529b2db7c7af13bb97339842af8086ec4e --- /dev/null +++ b/pkg/registry/testdata/part/incubator/apache/apache.libsonnet @@ -0,0 +1,107 @@ +local k = import "k.libsonnet"; +local deployment = k.extensions.v1beta1.deployment; + +{ + parts::{ + svc(namespace, name, selector={app: name}):: { + apiVersion: "v1", + kind: "Service", + metadata: { + name: name, + namespace: namespace, + labels: { + app: name + }, + }, + spec: { + type: "LoadBalancer", + ports: [ + { + name: "http", + port: 80, + targetPort: "http", + }, + { + name: "https", + port: 443, + targetPort: "https", + }, + ], + selector: selector + }, + }, + + deployment(namespace, name, labels={app: name})::{ + local defaults = { + // ref: https://hub.docker.com/r/bitnami/apache/tags/ + imageTag:: "2.4.23-r12", + // ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + imagePullPolicy:: "IfNotPresent", + }, + apiVersion: "extensions/v1beta1", + kind: "Deployment", + metadata: { + namespace: namespace, + name: name, + labels: labels + }, + spec: { + replicas: 1, + template: { + metadata: { + labels: labels + }, + spec: { + containers: [ + { + name: name, + image: "bitnami/apache:%s" % defaults.imageTag, + imagePullPolicy: defaults.imagePullPolicy, + ports: [ + { + name: "http", + containerPort: 80, + }, + { + name: "https", + containerPort: 443, + } + ], + livenessProbe: { + httpGet: { + path: "/", + port: "http", + }, + initialDelaySeconds: 30, + timeoutSeconds: 5, + failureThreshold: 6, + }, + readinessProbe: { + httpGet: { + path: "/", + port: "http", + }, + initialDelaySeconds: 5, + timeoutSeconds: 3, + periodSeconds: 5, + }, + volumeMounts: [ + { + name: "apache-data", + mountPath: "/bitnami/apache", + } + ], + } + ], + volumes: [ + { + name: "apache-data", + emptyDir: {}, + } + ] + }, + }, + }, + }, + }, +} diff --git a/pkg/registry/testdata/part/incubator/apache/examples/apache.jsonnet b/pkg/registry/testdata/part/incubator/apache/examples/apache.jsonnet new file mode 100644 index 0000000000000000000000000000000000000000..523d9d0c9ff05486217bb3b67b579732131a00b2 --- /dev/null +++ b/pkg/registry/testdata/part/incubator/apache/examples/apache.jsonnet @@ -0,0 +1,13 @@ +local k = import "k.libsonnet"; +local apache = import "../apache.libsonnet"; + + +local namespace = "default"; +local name = "apache-app"; + +k.core.v1.list.new( + [ + apache.parts.deployment(namespace, name), + apache.parts.svc(namespace, name) + ] +) diff --git a/pkg/registry/testdata/part/incubator/apache/examples/generated.yaml b/pkg/registry/testdata/part/incubator/apache/examples/generated.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f92073ce7d512387fe5d88eb79aa7e367bba8f77 --- /dev/null +++ b/pkg/registry/testdata/part/incubator/apache/examples/generated.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: apache-app + name: apache-app +spec: + replicas: 1 + template: + metadata: + labels: + app: apache-app + spec: + containers: + - image: bitnami/apache:2.4.23-r12 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 6 + httpGet: + path: / + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + name: apache-app + ports: + - containerPort: 80 + name: http + - containerPort: 443 + name: https + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + volumeMounts: + - mountPath: /bitnami/apache + name: apache-data + volumes: + - emptyDir: {} + name: apache-data +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: apache-app + name: apache-app +spec: + ports: + - name: http + port: 80 + targetPort: http + - name: https + port: "443" + targetPort: https + selector: + app: apache-app + type: LoadBalancer \ No newline at end of file diff --git a/pkg/registry/testdata/part/incubator/apache/parts.yaml b/pkg/registry/testdata/part/incubator/apache/parts.yaml new file mode 100644 index 0000000000000000000000000000000000000000..50e38ed13ae8cf2977189480e9dc5852945a0d2a --- /dev/null +++ b/pkg/registry/testdata/part/incubator/apache/parts.yaml @@ -0,0 +1,40 @@ +{ + "name": "apache", + "apiVersion": "0.0.1", + "kind": "ksonnet.io/parts", + "description": "part description", + "author": "author", + "contributors": [ + { + "name": "author 1", + "email": "email@example.com" + }, + { + "name": "author 2", + "email": "email@example.com" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/ksonnet/mixins" + }, + "bugs": { + "url": "https://github.com/ksonnet/mixins/issues" + }, + "keywords": [ + "apache", + "server", + "http" + ], + "quickStart": { + "prototype": "io.ksonnet.pkg.apache-simple", + "componentName": "apache", + "flags": { + "name": "apache", + "namespace": "default" + }, + "comment": "Run a simple Apache server" + }, + "license": "Apache 2.0" +} + diff --git a/pkg/registry/testdata/part/incubator/apache/prototypes/apache-simple.jsonnet b/pkg/registry/testdata/part/incubator/apache/prototypes/apache-simple.jsonnet new file mode 100644 index 0000000000000000000000000000000000000000..b3fce3b8e8066854780410e1b5005dd7a17324fd --- /dev/null +++ b/pkg/registry/testdata/part/incubator/apache/prototypes/apache-simple.jsonnet @@ -0,0 +1,18 @@ +// @apiVersion 0.0.1 +// @name io.ksonnet.pkg.apache-simple +// @description Apache HTTP Server. Apache is deployed using a deployment, and exposed to the +// network using a service. +// @shortDescription A simple, stateless Apache HTTP server. +// @param namespace string Namespace to divvy up your cluster; default is 'default' +// @param name string Name to identify all Kubernetes objects in this prototype + +local k = import 'k.libsonnet'; +local apache = import 'incubator/apache/apache.libsonnet'; + +local namespace = import 'param://namespace'; +local appName = import 'param://name'; + +k.core.v1.list.new([ + apache.parts.deployment(namespace, appName), + apache.parts.svc(namespace, appName) +]) diff --git a/pkg/registry/testdata/registry.yaml b/pkg/registry/testdata/registry.yaml new file mode 100644 index 0000000000000000000000000000000000000000..82c71a8153462939c37f6bee3d1ecb6c95666418 --- /dev/null +++ b/pkg/registry/testdata/registry.yaml @@ -0,0 +1,9 @@ +apiVersion: 0.1.0 +gitVersion: + commitSha: 40285d8a14f1ac5787e405e1023cf0c07f6aa28c + refSpec: master +kind: ksonnet.io/registry +libraries: + apache: + path: apache + version: master diff --git a/pkg/util/github/github.go b/pkg/util/github/github.go new file mode 100644 index 0000000000000000000000000000000000000000..c9eebc1b09b46bdc2ee4fc41814dc4ef91659580 --- /dev/null +++ b/pkg/util/github/github.go @@ -0,0 +1,84 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 github + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/google/go-github/github" + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" +) + +const ( + registryYAMLFile = "registry.yaml" + defaultGitHubBranch = "master" +) + +var ( + // DefaultClient is the default GitHub client. + DefaultClient = &defaultGitHub{} + + errInvalidURI = fmt.Errorf("Invalid GitHub URI: try navigating in GitHub to the URI of the folder containing the 'yaml', and using that URI instead. Generally, this URI should be of the form 'github.com/{organization}/{repository}/tree/{branch}/[path-to-directory]'") +) + +// Repo is a GitHub repo +type Repo struct { + Org string + Repo string +} + +// GitHub is an interface for communicating with GitHub. +type GitHub interface { + CommitSHA1(ctx context.Context, repo Repo, refSpec string) (string, error) + Contents(ctx context.Context, repo Repo, path, sha1 string) (*github.RepositoryContent, []*github.RepositoryContent, error) +} + +type defaultGitHub struct{} + +var _ GitHub = (*defaultGitHub)(nil) + +func (dg *defaultGitHub) CommitSHA1(ctx context.Context, repo Repo, refSpec string) (string, error) { + logrus.Debugf("github: fetching SHA1 for %s/%s - %s", repo.Org, repo.Repo, refSpec) + sha, _, err := dg.client().Repositories.GetCommitSHA1(ctx, repo.Org, repo.Repo, refSpec, "") + return sha, err +} + +func (dg *defaultGitHub) Contents(ctx context.Context, repo Repo, path, sha1 string) (*github.RepositoryContent, []*github.RepositoryContent, error) { + logrus.Debugf("github: fetching contents for %s/%s/%s - %s", repo.Org, repo.Repo, path, sha1) + opts := &github.RepositoryContentGetOptions{Ref: sha1} + + file, dir, _, err := dg.client().Repositories.GetContents(ctx, repo.Org, repo.Repo, path, opts) + return file, dir, err +} + +func (dg *defaultGitHub) client() *github.Client { + var hc *http.Client + + ght := os.Getenv("GITHUB_TOKEN") + if len(ght) > 0 { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ght}, + ) + hc = oauth2.NewClient(ctx, ts) + } + + return github.NewClient(hc) +} diff --git a/pkg/util/github/mocks/GitHub.go b/pkg/util/github/mocks/GitHub.go new file mode 100644 index 0000000000000000000000000000000000000000..2a6ac04254a6afb2631d0317bbfacb0546e04789 --- /dev/null +++ b/pkg/util/github/mocks/GitHub.go @@ -0,0 +1,80 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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. + +// Code generated by mockery v1.0.0 +package mocks + +import context "context" +import github "github.com/ksonnet/ksonnet/pkg/util/github" +import go_githubgithub "github.com/google/go-github/github" +import mock "github.com/stretchr/testify/mock" + +// GitHub is an autogenerated mock type for the GitHub type +type GitHub struct { + mock.Mock +} + +// CommitSHA1 provides a mock function with given fields: ctx, repo, refSpec +func (_m *GitHub) CommitSHA1(ctx context.Context, repo github.Repo, refSpec string) (string, error) { + ret := _m.Called(ctx, repo, refSpec) + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, github.Repo, string) string); ok { + r0 = rf(ctx, repo, refSpec) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, github.Repo, string) error); ok { + r1 = rf(ctx, repo, refSpec) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Contents provides a mock function with given fields: ctx, repo, path, sha1 +func (_m *GitHub) Contents(ctx context.Context, repo github.Repo, path string, sha1 string) (*go_githubgithub.RepositoryContent, []*go_githubgithub.RepositoryContent, error) { + ret := _m.Called(ctx, repo, path, sha1) + + var r0 *go_githubgithub.RepositoryContent + if rf, ok := ret.Get(0).(func(context.Context, github.Repo, string, string) *go_githubgithub.RepositoryContent); ok { + r0 = rf(ctx, repo, path, sha1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*go_githubgithub.RepositoryContent) + } + } + + var r1 []*go_githubgithub.RepositoryContent + if rf, ok := ret.Get(1).(func(context.Context, github.Repo, string, string) []*go_githubgithub.RepositoryContent); ok { + r1 = rf(ctx, repo, path, sha1) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]*go_githubgithub.RepositoryContent) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, github.Repo, string, string) error); ok { + r2 = rf(ctx, repo, path, sha1) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/pkg/util/test/test.go b/pkg/util/test/test.go new file mode 100644 index 0000000000000000000000000000000000000000..80062739ca42b6dbb7d3c3c9ac509f4cbdc834a7 --- /dev/null +++ b/pkg/util/test/test.go @@ -0,0 +1,41 @@ +// Copyright 2018 The ksonnet 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 +// +// http://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 test + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +// StageFile stages a file on on the provided filesystem from +// testdata. +func StageFile(t *testing.T, fs afero.Fs, src, dest string) { + in := filepath.Join("testdata", src) + + b, err := ioutil.ReadFile(in) + require.NoError(t, err) + + dir := filepath.Dir(dest) + err = fs.MkdirAll(dir, 0755) + require.NoError(t, err) + + err = afero.WriteFile(fs, dest, b, 0644) + require.NoError(t, err) +}