Skip to content

Commit

Permalink
feat: aod cli do/cleanup command (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
sqin2019 authored Jun 26, 2023
1 parent 490e6a9 commit dc4994f
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 31 deletions.
147 changes: 147 additions & 0 deletions pkg/cli/cli_handle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright 2023 The Authors (see AUTHORS file)
//
// 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 cli

import (
"context"
"fmt"

"github.com/abcxyz/access-on-demand/apis/v1alpha1"
"github.com/abcxyz/access-on-demand/pkg/handler"
"github.com/abcxyz/access-on-demand/pkg/requestutil"
"github.com/abcxyz/pkg/cli"
"github.com/posener/complete/v2/predict"
)

var _ cli.Command = (*CLIHandleCommand)(nil)

// CLIHandleCommand handles CLI requests.
type CLIHandleCommand struct {
cli.BaseCommand

flagPath string

flagDebug bool

// Run Cleanup instead of Do if true.
Cleanup bool

// testCLI is used for testing only.
testCLI string
}

func (c *CLIHandleCommand) Desc() string {
return `Handle the CLI request YAML file at the given path`
}

func (c *CLIHandleCommand) Help() string {
return `
Usage: {{ COMMAND }} [options]
Run "do" commands in the CLI request YAML file at the given path:
aod cli do -path "/path/to/file.yaml"
Run "do" commands in the CLI request YAML file at the given path in debug mode:
aod cli do -path "/path/to/file.yaml" -debug
Run "cleanup" commands in the CLI request YAML file at the given path:
aod cli cleanup -path "/path/to/file.yaml"
Run "cleanup" commands in the CLI request YAML file at the given path in debug mode:
aod cli cleanup -path "/path/to/file.yaml" -debug
`
}

func (c *CLIHandleCommand) Flags() *cli.FlagSet {
set := cli.NewFlagSet()

// Command options
f := set.NewSection("COMMAND OPTIONS")

f.StringVar(&cli.StringVar{
Name: "path",
Target: &c.flagPath,
Example: "/path/to/file.yaml",
Predict: predict.Files("*"),
Usage: `The path of CLI request file, in YAML format.`,
})

f.BoolVar(&cli.BoolVar{
Name: "debug",
Target: &c.flagDebug,
Default: false,
Usage: `Turn on debug mode to print command outputs.`,
})

return set
}

func (c *CLIHandleCommand) Run(ctx context.Context, args []string) error {
f := c.Flags()
if err := f.Parse(args); err != nil {
return fmt.Errorf("failed to parse flags: %w", err)
}
args = f.Args()
if len(args) > 0 {
return fmt.Errorf("unexpected arguments: %q", args)
}

if c.flagPath == "" {
return fmt.Errorf("path is required")
}

return c.handle(ctx)
}

func (c *CLIHandleCommand) handle(ctx context.Context) error {
// Read request from file path.
var req v1alpha1.CLIRequest
if err := requestutil.ReadRequestFromPath(c.flagPath, &req); err != nil {
return fmt.Errorf("failed to read %T: %w", &req, err)
}

if err := v1alpha1.ValidateCLIRequest(&req); err != nil {
return fmt.Errorf("failed to validate %T: %w", &req, err)
}

opts := []handler.CLIHandlerOption{handler.WithStderr(c.Stderr())}
if c.flagDebug {
opts = append(opts, handler.WithDebugMode(c.Stdout()))
}
h := handler.NewCLIHandler(ctx, opts...)

// Use testCLI if it is for testing.
if c.testCLI != "" {
req.CLI = c.testCLI
}
var err error
if c.Cleanup {
err = h.Cleanup(ctx, &req)
} else {
err = h.Do(ctx, &req)
}
if err != nil {
return fmt.Errorf(`failed to run commands: %w`, err)
}
c.Outf(`Successfully completed commands`)

return nil
}
166 changes: 166 additions & 0 deletions pkg/cli/cli_handle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2023 The Authors (see AUTHORS file)
//
// 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 cli

import (
"context"
"os"
"path/filepath"
"strings"
"testing"

"github.com/abcxyz/pkg/logging"
"github.com/abcxyz/pkg/testutil"
"github.com/google/go-cmp/cmp"
)

func TestCLIHandleCommand(t *testing.T) {
t.Parallel()

// Set up CLI request file.
requestFileContentByName := map[string]string{
"valid.yaml": `
cli: 'gcloud'
do:
- 'do1'
- 'do2'
cleanup:
- 'cleanup1'
- 'cleanup2'
`,
"invalid-request.yaml": `
cli: 'cli_not_exist'
do:
- 'do'
cleanup:
- 'cleanup'
`,
"invalid.yaml": `bananas`,
}
dir := t.TempDir()
for name, content := range requestFileContentByName {
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}

cases := []struct {
name string
args []string
testCLI string
cleanup bool
expOut string
expErr string
expStdErr string
}{
{
name: "success_do",
args: []string{"-path", filepath.Join(dir, "valid.yaml")},
testCLI: "echo",
expOut: `Successfully completed commands`,
},
{
name: "success_do_with_debug",
args: []string{"-path", filepath.Join(dir, "valid.yaml"), "-debug"},
testCLI: "echo",
expOut: `
do1
do2
Successfully completed commands`,
},
{
name: "success_cleanup",
args: []string{"-path", filepath.Join(dir, "valid.yaml")},
testCLI: "echo",
cleanup: true,
expOut: `Successfully completed commands`,
},
{
name: "success_cleanup_with_debug",
args: []string{"-path", filepath.Join(dir, "valid.yaml"), "-debug"},
testCLI: "echo",
cleanup: true,
expOut: `
cleanup1
cleanup2
Successfully completed commands`,
},
{
name: "unexpected_args",
args: []string{"foo"},
expErr: `unexpected arguments: ["foo"]`,
},
{
name: "missing_path",
args: []string{},
expErr: `path is required`,
},
{
name: "invalid_yaml",
args: []string{"-path", filepath.Join(dir, "invalid.yaml")},
expErr: "failed to read *v1alpha1.CLIRequest",
},
{
name: "handler_do_failure",
args: []string{"-path", filepath.Join(dir, "valid.yaml")},
testCLI: "ls",
expErr: `failed to run command "do1"`,
expStdErr: "ls: cannot access 'do1': No such file or directory",
},
{
name: "handler_cleanup_failure",
args: []string{"-path", filepath.Join(dir, "valid.yaml")},
testCLI: "ls",
cleanup: true,
expErr: `failed to run command "cleanup1"`,
expStdErr: "ls: cannot access 'cleanup1': No such file or directory",
},
{
name: "invalid_request",
args: []string{"-path", filepath.Join(dir, "invalid-request.yaml")},
expErr: "failed to validate *v1alpha1.CLIRequest",
},
}

for _, tc := range cases {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

ctx := logging.WithLogger(context.Background(), logging.TestLogger(t))

cmd := CLIHandleCommand{
Cleanup: tc.cleanup,
testCLI: tc.testCLI,
}
_, stdout, stderr := cmd.Pipe()

args := append([]string{}, tc.args...)

err := cmd.Run(ctx, args)
if diff := testutil.DiffErrString(err, tc.expErr); diff != "" {
t.Errorf("Process(%+v) got error diff (-want, +got):\n%s", tc.name, diff)
}
if diff := cmp.Diff(strings.TrimSpace(tc.expOut), strings.TrimSpace(stdout.String())); diff != "" {
t.Errorf("Process(%+v) got output diff (-want, +got):\n%s", tc.name, diff)
}
if diff := cmp.Diff(strings.TrimSpace(tc.expStdErr), strings.TrimSpace(stderr.String())); diff != "" {
t.Errorf("Process(%+v) got command error diff (-want, +got):\n%s", tc.name, diff)
}
})
}
}
12 changes: 6 additions & 6 deletions pkg/cli/iam_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ func (c *IAMHandleCommand) Run(ctx context.Context, args []string) error {

func (c *IAMHandleCommand) handleIAM(ctx context.Context) error {
// Read request from file path.
req, err := requestutil.ReadFromPath(c.flagPath)
if err != nil {
return fmt.Errorf("failed to read %T: %w", req, err)
var req v1alpha1.IAMRequest
if err := requestutil.ReadRequestFromPath(c.flagPath, &req); err != nil {
return fmt.Errorf("failed to read %T: %w", &req, err)
}

if err := v1alpha1.ValidateIAMRequest(req); err != nil {
return fmt.Errorf("failed to validate %T: %w", req, err)
if err := v1alpha1.ValidateIAMRequest(&req); err != nil {
return fmt.Errorf("failed to validate %T: %w", &req, err)
}

var h iamHandler
Expand Down Expand Up @@ -170,7 +170,7 @@ func (c *IAMHandleCommand) handleIAM(ctx context.Context) error {

// Wrap IAMRequest to include Duration.
reqWrapper := &v1alpha1.IAMRequestWrapper{
IAMRequest: req,
IAMRequest: &req,
Duration: c.flagDuration,
StartTime: c.flagStartTime,
}
Expand Down
13 changes: 6 additions & 7 deletions pkg/cli/iam_handle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,12 @@ policies:
st := time.Now().UTC().Round(time.Second)

cases := []struct {
name string
args []string
fileData []byte
handler *fakeIAMHandler
expReq *v1alpha1.IAMRequestWrapper
expOut string
expErr string
name string
args []string
handler *fakeIAMHandler
expReq *v1alpha1.IAMRequestWrapper
expOut string
expErr string
}{
{
name: "success",
Expand Down
10 changes: 5 additions & 5 deletions pkg/cli/iam_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ func (c *IAMValidateCommand) Run(ctx context.Context, args []string) error {

func (c *IAMValidateCommand) validate(ctx context.Context) error {
// Read request from YAML file.
req, err := requestutil.ReadFromPath(c.flagPath)
if err != nil {
return fmt.Errorf("failed to read %T: %w", req, err)
var req v1alpha1.IAMRequest
if err := requestutil.ReadRequestFromPath(c.flagPath, &req); err != nil {
return fmt.Errorf("failed to read %T: %w", &req, err)
}

if err := v1alpha1.ValidateIAMRequest(req); err != nil {
return fmt.Errorf("failed to validate %T: %w", req, err)
if err := v1alpha1.ValidateIAMRequest(&req); err != nil {
return fmt.Errorf("failed to validate %T: %w", &req, err)
}
c.Outf("Successfully validated IAM request")

Expand Down
Loading

0 comments on commit dc4994f

Please sign in to comment.