diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 43e996590c..2b70c73f27 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,17 +12,22 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + - uses: actions/cache@v2 with: - fetch-depth: "0" + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }} - name: Run GoReleaser dry run uses: goreleaser/goreleaser-action@v2 with: version: latest - args: --snapshot --skip-publish --rm-dist + args: --snapshot --skip-publish --rm-dist - name: Unit Tests uses: cedrickring/golang-action@1.5.2 env: GO111MODULE: "on" + CI_ENV: "true" with: args: make install && make test_unit_codecov - name: Push CodeCov diff --git a/cmd/config/subcommand/sandbox/config_flags.go b/cmd/config/subcommand/sandbox/config_flags.go new file mode 100755 index 0000000000..3c1cd8d74e --- /dev/null +++ b/cmd/config/subcommand/sandbox/config_flags.go @@ -0,0 +1,55 @@ +// Code generated by go generate; DO NOT EDIT. +// This file was generated by robots. + +package sandbox + +import ( + "encoding/json" + "reflect" + + "fmt" + + "github.com/spf13/pflag" +) + +// If v is a pointer, it will get its element value or the zero value of the element type. +// If v is not a pointer, it will return it as is. +func (Config) elemValueOrNil(v interface{}) interface{} { + if t := reflect.TypeOf(v); t.Kind() == reflect.Ptr { + if reflect.ValueOf(v).IsNil() { + return reflect.Zero(t.Elem()).Interface() + } else { + return reflect.ValueOf(v).Interface() + } + } else if v == nil { + return reflect.Zero(t).Interface() + } + + return v +} + +func (Config) mustJsonMarshal(v interface{}) string { + raw, err := json.Marshal(v) + if err != nil { + panic(err) + } + + return string(raw) +} + +func (Config) mustMarshalJSON(v json.Marshaler) string { + raw, err := v.MarshalJSON() + if err != nil { + panic(err) + } + + return string(raw) +} + +// GetPFlagSet will return strongly types pflags for all fields in Config and its nested types. The format of the +// flags is json-name.json-sub-name... etc. +func (cfg Config) GetPFlagSet(prefix string) *pflag.FlagSet { + cmdFlags := pflag.NewFlagSet("Config", pflag.ExitOnError) + cmdFlags.StringVar(&DefaultConfig.SnacksRepo, fmt.Sprintf("%v%v", prefix, "flytesnacks"), DefaultConfig.SnacksRepo, " Path of your flytesnacks repository") + return cmdFlags +} diff --git a/cmd/config/subcommand/sandbox/config_flags_test.go b/cmd/config/subcommand/sandbox/config_flags_test.go new file mode 100755 index 0000000000..c6bf24684c --- /dev/null +++ b/cmd/config/subcommand/sandbox/config_flags_test.go @@ -0,0 +1,116 @@ +// Code generated by go generate; DO NOT EDIT. +// This file was generated by robots. + +package sandbox + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/mitchellh/mapstructure" + "github.com/stretchr/testify/assert" +) + +var dereferencableKindsConfig = map[reflect.Kind]struct{}{ + reflect.Array: {}, reflect.Chan: {}, reflect.Map: {}, reflect.Ptr: {}, reflect.Slice: {}, +} + +// Checks if t is a kind that can be dereferenced to get its underlying type. +func canGetElementConfig(t reflect.Kind) bool { + _, exists := dereferencableKindsConfig[t] + return exists +} + +// This decoder hook tests types for json unmarshaling capability. If implemented, it uses json unmarshal to build the +// object. Otherwise, it'll just pass on the original data. +func jsonUnmarshalerHookConfig(_, to reflect.Type, data interface{}) (interface{}, error) { + unmarshalerType := reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() + if to.Implements(unmarshalerType) || reflect.PtrTo(to).Implements(unmarshalerType) || + (canGetElementConfig(to.Kind()) && to.Elem().Implements(unmarshalerType)) { + + raw, err := json.Marshal(data) + if err != nil { + fmt.Printf("Failed to marshal Data: %v. Error: %v. Skipping jsonUnmarshalHook", data, err) + return data, nil + } + + res := reflect.New(to).Interface() + err = json.Unmarshal(raw, &res) + if err != nil { + fmt.Printf("Failed to umarshal Data: %v. Error: %v. Skipping jsonUnmarshalHook", data, err) + return data, nil + } + + return res, nil + } + + return data, nil +} + +func decode_Config(input, result interface{}) error { + config := &mapstructure.DecoderConfig{ + TagName: "json", + WeaklyTypedInput: true, + Result: result, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + jsonUnmarshalerHookConfig, + ), + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + +func join_Config(arr interface{}, sep string) string { + listValue := reflect.ValueOf(arr) + strs := make([]string, 0, listValue.Len()) + for i := 0; i < listValue.Len(); i++ { + strs = append(strs, fmt.Sprintf("%v", listValue.Index(i))) + } + + return strings.Join(strs, sep) +} + +func testDecodeJson_Config(t *testing.T, val, result interface{}) { + assert.NoError(t, decode_Config(val, result)) +} + +func testDecodeRaw_Config(t *testing.T, vStringSlice, result interface{}) { + assert.NoError(t, decode_Config(vStringSlice, result)) +} + +func TestConfig_GetPFlagSet(t *testing.T) { + val := Config{} + cmdFlags := val.GetPFlagSet("") + assert.True(t, cmdFlags.HasFlags()) +} + +func TestConfig_SetFlags(t *testing.T) { + actual := Config{} + cmdFlags := actual.GetPFlagSet("") + assert.True(t, cmdFlags.HasFlags()) + + t.Run("Test_flytesnacks", func(t *testing.T) { + + t.Run("Override", func(t *testing.T) { + testValue := "1" + + cmdFlags.Set("flytesnacks", testValue) + if vString, err := cmdFlags.GetString("flytesnacks"); err == nil { + testDecodeJson_Config(t, fmt.Sprintf("%v", vString), &actual.SnacksRepo) + + } else { + assert.FailNow(t, err.Error()) + } + }) + }) +} diff --git a/cmd/config/subcommand/sandbox/sandbox_config.go b/cmd/config/subcommand/sandbox/sandbox_config.go new file mode 100644 index 0000000000..e9f8098828 --- /dev/null +++ b/cmd/config/subcommand/sandbox/sandbox_config.go @@ -0,0 +1,11 @@ +package sandbox + +//go:generate pflags Config --default-var DefaultConfig +var ( + DefaultConfig = &Config{} +) + +// Config +type Config struct { + SnacksRepo string `json:"flytesnacks" pflag:", Path of your flytesnacks repository"` +} diff --git a/cmd/sandbox/sandbox.go b/cmd/sandbox/sandbox.go index 995c6c6890..cf666b9a48 100644 --- a/cmd/sandbox/sandbox.go +++ b/cmd/sandbox/sandbox.go @@ -1,6 +1,7 @@ package sandbox import ( + sandboxConfig "github.com/flyteorg/flytectl/cmd/config/subcommand/sandbox" cmdcore "github.com/flyteorg/flytectl/cmd/core" "github.com/spf13/cobra" ) @@ -33,7 +34,7 @@ func CreateSandboxCommand() *cobra.Command { sandboxResourcesFuncs := map[string]cmdcore.CommandEntry{ "start": {CmdFunc: startSandboxCluster, Aliases: []string{}, ProjectDomainNotRequired: true, Short: startShort, - Long: startLong}, + Long: startLong, PFlagProvider: sandboxConfig.DefaultConfig}, "teardown": {CmdFunc: teardownSandboxCluster, Aliases: []string{}, ProjectDomainNotRequired: true, Short: teardownShort, Long: teardownLong}, diff --git a/cmd/sandbox/sandbox_util.go b/cmd/sandbox/sandbox_util.go index d739e671a7..e6c0d43944 100644 --- a/cmd/sandbox/sandbox_util.go +++ b/cmd/sandbox/sandbox_util.go @@ -10,26 +10,32 @@ import ( "os" "strings" + cmdUtil "github.com/flyteorg/flytectl/pkg/commandutils" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" - "github.com/enescakir/emoji" f "github.com/flyteorg/flytectl/pkg/filesystemutils" ) var ( - Kubeconfig = f.FilePathJoin(f.UserHomeDir(), ".flyte", "k3s", "k3s.yaml") - FlytectlConfig = f.FilePathJoin(f.UserHomeDir(), ".flyte", "config.yaml") - SuccessMessage = "Flyte is ready! Flyte UI is available at http://localhost:30081/console" - ImageName = "ghcr.io/flyteorg/flyte-sandbox:dind" - SandboxClusterName = "flyte-sandbox" - Environment = []string{"SANDBOX=1", "KUBERNETES_API_PORT=30086", "FLYTE_HOST=localhost:30081", "FLYTE_AWS_ENDPOINT=http://localhost:30084"} + Kubeconfig = f.FilePathJoin(f.UserHomeDir(), ".flyte", "k3s", "k3s.yaml") + FlytectlConfig = f.FilePathJoin(f.UserHomeDir(), ".flyte", "config-sandbox.yaml") + SuccessMessage = "Flyte is ready! Flyte UI is available at http://localhost:30081/console" + ImageName = "ghcr.io/flyteorg/flyte-sandbox:dind" + flyteSandboxClusterName = "flyte-sandbox" + Environment = []string{"SANDBOX=1", "KUBERNETES_API_PORT=30086", "FLYTE_HOST=localhost:30081", "FLYTE_AWS_ENDPOINT=http://localhost:30084"} + flyteSnackDir = "/usr/src" + K3sDir = "/etc/rancher/" ) func setupFlytectlConfig() error { + + _ = os.MkdirAll(f.FilePathJoin(f.UserHomeDir(), ".flyte"), 0755) + response, err := http.Get("https://raw.githubusercontent.com/flyteorg/flytectl/master/config.yaml") if err != nil { return err @@ -41,11 +47,7 @@ func setupFlytectlConfig() error { return err } - err = ioutil.WriteFile(FlytectlConfig, data, 0600) - if err != nil { - fmt.Printf("Please create ~/.flyte dir %v \n", emoji.ManTechnologist) - return err - } + _ = ioutil.WriteFile(FlytectlConfig, data, 0600) return nil } @@ -62,21 +64,31 @@ func configCleanup() error { } func getSandbox(cli *client.Client) *types.Container { - containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{ + containers, _ := cli.ContainerList(context.Background(), types.ContainerListOptions{ All: true, }) - if err != nil { - return nil - } for _, v := range containers { - if strings.Contains(v.Names[0], SandboxClusterName) { + if strings.Contains(v.Names[0], flyteSandboxClusterName) { return &v } } return nil } -func startContainer(cli *client.Client) (string, error) { +func removeSandboxIfExist(cli *client.Client, reader io.Reader) error { + if c := getSandbox(cli); c != nil { + if cmdUtil.AskForConfirmation("delete existing sandbox cluster", reader) { + err := cli.ContainerRemove(context.Background(), c.ID, types.ContainerRemoveOptions{ + Force: true, + }) + return err + } + os.Exit(0) + } + return nil +} + +func startContainer(cli *client.Client, volumes []mount.Mount) (string, error) { ExposedPorts, PortBindings, _ := nat.ParsePortSpecs([]string{ "127.0.0.1:30086:30086", "127.0.0.1:30081:30081", @@ -87,38 +99,23 @@ func startContainer(cli *client.Client) (string, error) { if err != nil { return "", err } - - if _, err := io.Copy(os.Stdout, r); err != nil { - return "", err - } - + _, _ = io.Copy(os.Stdout, r) resp, err := cli.ContainerCreate(context.Background(), &container.Config{ Env: Environment, Image: ImageName, Tty: false, ExposedPorts: ExposedPorts, }, &container.HostConfig{ - Mounts: []mount.Mount{ - { - Type: mount.TypeBind, - Source: f.FilePathJoin(f.UserHomeDir(), ".flyte"), - Target: "/etc/rancher/", - }, - // TODO (Yuvraj) Add flytectl config in sandbox and mount with host file system - //{ - // Type: mount.TypeBind, - // Source: f.FilePathJoin(f.UserHomeDir(), ".flyte", "config.yaml"), - // Target: "/.flyte/", - //}, - }, + Mounts: volumes, PortBindings: PortBindings, Privileged: true, }, nil, - nil, SandboxClusterName) + nil, flyteSandboxClusterName) + if err != nil { return "", err } - + go watchError(cli, resp.ID) if err := cli.ContainerStart(context.Background(), resp.ID, types.ContainerStartOptions{}); err != nil { return "", err } @@ -127,6 +124,7 @@ func startContainer(cli *client.Client) (string, error) { func watchError(cli *client.Client, id string) { statusCh, errCh := cli.ContainerWait(context.Background(), id, container.WaitConditionNotRunning) + select { case err := <-errCh: if err != nil { @@ -136,7 +134,7 @@ func watchError(cli *client.Client, id string) { } } -func readLogs(cli *client.Client, id string) error { +func readLogs(cli *client.Client, id, message string) error { reader, err := cli.ContainerLogs(context.Background(), id, types.ContainerLogsOptions{ ShowStderr: true, ShowStdout: true, @@ -147,9 +145,10 @@ func readLogs(cli *client.Client, id string) error { return err } scanner := bufio.NewScanner(reader) + for scanner.Scan() { - if strings.Contains(scanner.Text(), SuccessMessage) { - fmt.Printf("%v %v %v %v %v \n", emoji.ManTechnologist, SuccessMessage, emoji.Rocket, emoji.Rocket, emoji.PartyPopper) + if strings.Contains(scanner.Text(), message) { + fmt.Printf("%v %v %v %v %v \n", emoji.ManTechnologist, message, emoji.Rocket, emoji.Rocket, emoji.PartyPopper) fmt.Printf("Please visit https://github.com/flyteorg/flytesnacks for more example %v \n", emoji.Rocket) fmt.Printf("Register all flytesnacks example by running 'flytectl register examples -d development -p flytesnacks' \n") break diff --git a/cmd/sandbox/sandbox_util_test.go b/cmd/sandbox/sandbox_util_test.go index 269152a9ba..ab7e981b98 100644 --- a/cmd/sandbox/sandbox_util_test.go +++ b/cmd/sandbox/sandbox_util_test.go @@ -8,7 +8,9 @@ import ( "testing" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" + sandboxConfig "github.com/flyteorg/flytectl/cmd/config/subcommand/sandbox" cmdCore "github.com/flyteorg/flytectl/cmd/core" u "github.com/flyteorg/flytectl/cmd/testutils" @@ -29,8 +31,10 @@ func cleanup(client *client.Client) error { return err } for _, v := range containers { - if strings.Contains(v.Names[0], SandboxClusterName) { - if err := client.ContainerRemove(context.Background(), v.ID, types.ContainerRemoveOptions{}); err != nil { + if strings.Contains(v.Names[0], flyteSandboxClusterName) { + if err := client.ContainerRemove(context.Background(), v.ID, types.ContainerRemoveOptions{ + Force: true, + }); err != nil { return err } } @@ -41,10 +45,6 @@ func cleanup(client *client.Client) error { func setupSandbox() { mockAdminClient := u.MockClient cmdCtx = cmdCore.NewCommandContext(mockAdminClient, u.MockOutStream) - _, err := os.Stat(f.FilePathJoin(f.UserHomeDir(), ".flyte")) - if os.IsNotExist(err) { - _ = os.MkdirAll(f.FilePathJoin(f.UserHomeDir(), ".flyte"), 0755) - } _ = setupFlytectlConfig() } @@ -81,6 +81,7 @@ func TestSetupFlytectlConfig(t *testing.T) { check := os.IsNotExist(err) assert.Equal(t, check, false) _ = configCleanup() + } func TestTearDownSandbox(t *testing.T) { @@ -89,12 +90,47 @@ func TestTearDownSandbox(t *testing.T) { err := teardownSandboxCluster(context.Background(), []string{}, cmdCtx) assert.Nil(t, err) assert.Nil(t, cleanup(cli)) + + volumes = []mount.Mount{} + _ = startSandboxCluster(context.Background(), []string{}, cmdCtx) + err = teardownSandboxCluster(context.Background(), []string{}, cmdCtx) + assert.Nil(t, err) + } -func TestStartContainer(t *testing.T) { - setupSandbox() +func TestStartSandbox(t *testing.T) { cli, _ := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + assert.Nil(t, cleanup(cli)) + setupSandbox() + volumes = []mount.Mount{} + sandboxConfig.DefaultConfig.SnacksRepo = "/tmp" err := startSandboxCluster(context.Background(), []string{}, cmdCtx) assert.Nil(t, err) + + assert.Nil(t, cleanup(cli)) + setupSandbox() + sandboxConfig.DefaultConfig.SnacksRepo = f.UserHomeDir() + err = startSandboxCluster(context.Background(), []string{}, cmdCtx) + assert.NotNil(t, err) + + assert.Nil(t, cleanup(cli)) + _, err = startContainer(cli, []mount.Mount{}) + assert.Nil(t, err) + + assert.Nil(t, cleanup(cli)) + ImageName = "" + _, err = startContainer(cli, []mount.Mount{}) + assert.NotNil(t, err) +} + +func TestGetSandbox(t *testing.T) { + cli, _ := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + assert.Nil(t, cleanup(cli)) + setupSandbox() + sandboxConfig.DefaultConfig.SnacksRepo = f.UserHomeDir() + _ = startSandboxCluster(context.Background(), []string{}, cmdCtx) + + container := removeSandboxIfExist(cli, strings.NewReader("y")) + assert.Nil(t, container) } diff --git a/cmd/sandbox/start.go b/cmd/sandbox/start.go index 6a620603dd..082c12a54c 100644 --- a/cmd/sandbox/start.go +++ b/cmd/sandbox/start.go @@ -5,10 +5,12 @@ import ( "fmt" "os" + "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" "github.com/enescakir/emoji" + sandboxConfig "github.com/flyteorg/flytectl/cmd/config/subcommand/sandbox" cmdCore "github.com/flyteorg/flytectl/cmd/core" - cmdUtil "github.com/flyteorg/flytectl/pkg/commandutils" + f "github.com/flyteorg/flytectl/pkg/filesystemutils" ) const ( @@ -17,12 +19,24 @@ const ( Start will run the flyte sandbox cluster inside a docker container and setup the config that is required :: - bin/flytectl start + bin/flytectl sandbox start + +Mount your flytesnacks repository code inside sandbox +:: + bin/flytectl sandbox start --flytesnacks=$HOME/flyteorg/flytesnacks Usage ` ) +var volumes = []mount.Mount{ + { + Type: mount.TypeBind, + Source: f.FilePathJoin(f.UserHomeDir(), ".flyte"), + Target: K3sDir, + }, +} + type ExecResult struct { StdOut string StdErr string @@ -41,34 +55,36 @@ func startSandboxCluster(ctx context.Context, args []string, cmdCtx cmdCore.Comm return err } - if container := getSandbox(cli); container != nil { - if cmdUtil.AskForConfirmation("delete existing sandbox cluster", os.Stdin) { - if err := teardownSandboxCluster(ctx, []string{}, cmdCtx); err != nil { - return err - } - } + if err := removeSandboxIfExist(cli, os.Stdin); err != nil { + return err } - ID, err := startContainer(cli) - if err == nil { - os.Setenv("KUBECONFIG", Kubeconfig) + if len(sandboxConfig.DefaultConfig.SnacksRepo) > 0 { + volumes = append(volumes, mount.Mount{ + Type: mount.TypeBind, + Source: sandboxConfig.DefaultConfig.SnacksRepo, + Target: flyteSnackDir, + }) + } - defer func() { - if r := recover(); r != nil { - fmt.Println("Something goes wrong with container status", r) - } - }() + os.Setenv("KUBECONFIG", Kubeconfig) + os.Setenv("FLYTECTL_CONFIG", FlytectlConfig) - go watchError(cli, ID) - if err := readLogs(cli, ID); err != nil { - return err + defer func() { + if r := recover(); r != nil { + fmt.Println("Something goes wrong with container status", r) } + }() - fmt.Printf("Add (KUBECONFIG) to your environment variabl \n") - fmt.Printf("export KUBECONFIG=%v \n", Kubeconfig) - return nil + ID, err := startContainer(cli, volumes) + if err != nil { + fmt.Println("Something goes wrong. We are not able to start sandbox container, Please check your docker client and try again ") + return fmt.Errorf("error: %v", err) } - fmt.Println("Something goes wrong. We are not able to start sandbox container, Please check your docker client and try again \n", emoji.Rocket) - fmt.Printf("error: %v", err) + + _ = readLogs(cli, ID, SuccessMessage) + fmt.Printf("Add KUBECONFIG and FLYTECTL_CONFIG to your environment variable \n") + fmt.Printf("export KUBECONFIG=%v \n", Kubeconfig) + fmt.Printf("export FLYTECTL_CONFIG=%v \n", FlytectlConfig) return nil } diff --git a/cmd/sandbox/teardown.go b/cmd/sandbox/teardown.go index 9e0c9c5d50..8afced1178 100644 --- a/cmd/sandbox/teardown.go +++ b/cmd/sandbox/teardown.go @@ -4,9 +4,9 @@ import ( "context" "fmt" + "github.com/docker/docker/api/types" "github.com/enescakir/emoji" - "github.com/docker/docker/api/types" "github.com/docker/docker/client" cmdCore "github.com/flyteorg/flytectl/cmd/core" ) @@ -18,12 +18,7 @@ Teardown will remove docker container and all the flyte config :: bin/flytectl sandbox teardown - -Stop will remove docker container and all the flyte config -:: - - bin/flytectl sandbox stop - + Usage ` @@ -36,15 +31,12 @@ func teardownSandboxCluster(ctx context.Context, args []string, cmdCtx cmdCore.C return err } - container := getSandbox(cli) - if container != nil { - if err := cli.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{ + c := getSandbox(cli) + if c != nil { + _ = cli.ContainerRemove(context.Background(), c.ID, types.ContainerRemoveOptions{ Force: true, - }); err != nil { - return err - } + }) } - if err := configCleanup(); err != nil { fmt.Printf("Config cleanup failed. Which Failed due to %v \n ", err) }