Skip to content

Commit

Permalink
Add API to set active UCP resource for UCP context
Browse files Browse the repository at this point in the history
- Add API to set active UCP resource for given UCP context. It would call the CLI command to set the active UCP resource for the given UCP context

Signed-off-by: Prem Kumar Kalle <[email protected]>
  • Loading branch information
prkalle committed Sep 29, 2023
1 parent 301f057 commit fe52a9b
Show file tree
Hide file tree
Showing 2 changed files with 298 additions and 2 deletions.
119 changes: 118 additions & 1 deletion ucp/ucp.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
package ucp

import (
"bytes"
"io"
"os"
"os/exec"
"strings"

"github.com/pkg/errors"
"gopkg.in/yaml.v3"

Expand All @@ -13,10 +19,78 @@ import (
"github.com/vmware-tanzu/tanzu-plugin-runtime/ucp/internal/kubeconfig"
)

// keys to Context's AdditionalMetadata map
const (
OrgIDKey = "ucpOrgID"
ProjectNameKey = "ucpProjectName"
SpaceNameKey = "ucpSpaceName"
)

const (
OrgID = "ucpOrgID"
// customCommandName is the name of the command expected to be implemented
// by the CLI should there be a need to discover and alternative invocation
// method
customCommandName string = "_custom_command"
)

// cmdOptions specifies the command options
type cmdOptions struct {
outWriter io.Writer
errWriter io.Writer
}

type CommandOptions func(o *cmdOptions)

// WithOutputWriter specifies the CommandOption for configuring Stdout
func WithOutputWriter(outWriter io.Writer) CommandOptions {
return func(o *cmdOptions) {
o.outWriter = outWriter
}
}

// WithErrorWriter specifies the CommandOption for configuring Stderr
func WithErrorWriter(errWriter io.Writer) CommandOptions {
return func(o *cmdOptions) {
o.errWriter = errWriter
}
}

// WithNoStdout specifies to ignore stdout
func WithNoStdout() CommandOptions {
return func(o *cmdOptions) {
o.outWriter = io.Discard
}
}

// WithNoStderr specifies to ignore stderr
func WithNoStderr() CommandOptions {
return func(o *cmdOptions) {
o.errWriter = io.Discard
}
}

func runCommand(commandPath string, args []string, opts *cmdOptions) (bytes.Buffer, bytes.Buffer, error) {
command := exec.Command(commandPath, args...)

var stderr bytes.Buffer
var stdout bytes.Buffer

wout := io.MultiWriter(&stdout, os.Stdout)
werr := io.MultiWriter(&stderr, os.Stderr)

if opts.outWriter != nil {
wout = io.MultiWriter(&stdout, opts.outWriter)
}
if opts.errWriter != nil {
werr = io.MultiWriter(&stderr, opts.errWriter)
}

command.Stdout = wout
command.Stderr = werr

return stdout, stderr, command.Run()
}

// GetKubeconfigForContext returns the kubeconfig for any arbitrary UCP resource in the UCP object hierarchy
// referred by the UCP context
// Pre-reqs: project and space names should be valid
Expand Down Expand Up @@ -79,3 +153,46 @@ func updateKubeconfigServerURL(kc *kubeconfig.Config, cliContext *configtypes.Co
cluster := kubeconfig.GetCluster(kc, context.Context.Cluster)
cluster.Cluster.Server = prepareClusterServerURL(cliContext, projectName, spaceName)
}

// SetUCPContextActiveResource sets the active UCP resource for the given context and also updates
// the kubeconfig referrenced by the UCP context
//
// Pre-reqs: project and space names should be valid
//
// Note: To set
// - a space as active resource, both project and space names are required
// - a project as active resource, only project name is required (space should be empty string)
// - org as active resource, both project and space names should be empty strings
func SetUCPContextActiveResource(contextName, projectName, spaceName string, opts ...CommandOptions) error {
// For now, the implementation expects env var TANZU_BIN to be set and
// pointing to the core CLI binary used to invoke setting the active UCP resource.

options := &cmdOptions{}
for _, opt := range opts {
opt(options)
}

cliPath := os.Getenv("TANZU_BIN")
if cliPath == "" {
return errors.New("the environment variable TANZU_BIN is not set")
}

altCommandArgs := []string{customCommandName}
args := []string{"context", "update", "ucp-active-resource", contextName, "--project", projectName, "--space", spaceName}

altCommandArgs = append(altCommandArgs, args...)

// Check if there is an alternate means to set the active UCP context active resource
// operation, if not fall back to `context update ucp-active-resource`
stdoutOutput, _, err := runCommand(cliPath, altCommandArgs, &cmdOptions{outWriter: io.Discard, errWriter: io.Discard})
if err == nil {
args = strings.Fields(stdoutOutput.String())
}

// Runs the actual command
_, stderrOutput, err := runCommand(cliPath, args, options)
if err != nil {
return errors.New(stderrOutput.String())
}
return nil
}
181 changes: 180 additions & 1 deletion ucp/ucp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
package ucp

import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -16,6 +20,58 @@ import (
)

const ConfigFilePermissions = 0o600
const (
fakePluginScriptFmtString string = `#!/bin/bash
# Fake tanzu core binary
# fake command that simulates a context lcm operation
context() {
if [ "%s" -eq "0" ]; then
# regular output to stderr
>&2 echo "$@ succeeded"
else
# error to stderr
>&2 echo "$@ failed"
fi
exit %s
}
# fake alternate command to use
newcommand() {
if [ "%s" -eq "0" ]; then
# regular output to stdout
echo "$@ succeeded"
else
# error to stderr
>&2 echo "$@ failed"
fi
exit %s
}
case "$1" in
# simulate returning an alternative set of args to invoke with, which
# translates to running the command 'newcommand'
%s) shift && shift && echo "newcommand $@";;
newcommand) $1 "$@";;
context) $1 "$@";;
*) cat << EOF
Tanzu Core CLI Fake
Usage:
tanzu [command]
Available Commands:
update fake command
newcommand fake new command
_custom_command provide alternate command to invoke, if available
EOF
exit 1
;;
esac
`
)

func cleanupTestingDir(t *testing.T) {
p, err := config.LocalDir()
Expand All @@ -30,6 +86,13 @@ func copyFile(t *testing.T, sourceFile, destFile string) {
err = os.WriteFile(destFile, input, ConfigFilePermissions)
assert.NoError(t, err)
}
func readOutput(t *testing.T, r io.Reader, c chan<- []byte) {
data, err := io.ReadAll(r)
if err != nil {
t.Error(err)
}
c <- data
}

func setupForGetContext(t *testing.T) {
// setup
Expand Down Expand Up @@ -74,7 +137,7 @@ func setupForGetContext(t *testing.T) {
Context: "test-context",
},
AdditionalMetadata: map[string]interface{}{
OrgID: "fake-org-id",
OrgIDKey: "fake-org-id",
},
},
},
Expand Down Expand Up @@ -139,3 +202,119 @@ func TestGetKubeconfigForContext(t *testing.T) {
assert.Error(t, err)
assert.ErrorContains(t, err, "context must be of type: ucp")
}

func setupFakeCLI(dir string, exitStatus string, newCommandExitStatus string, enableCustomCommand bool) (string, error) {
filePath := filepath.Join(dir, "tanzu")

f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
return "", err
}
defer f.Close()

fakeCustomCommandName := "unused_command"
// when enabled, the fake CLI script generated will be capable of
// returning an alternate set of args for a provided set of args
if enableCustomCommand {
fakeCustomCommandName = customCommandName
}

fmt.Fprintf(f, fakePluginScriptFmtString, exitStatus, exitStatus, newCommandExitStatus, newCommandExitStatus, fakeCustomCommandName)

return filePath, nil
}

func TestSetUCPContextActiveResource(t *testing.T) {
tests := []struct {
test string
exitStatus string
newCommandExitStatus string
expectedOutput string
expectedFailure bool
enableCustomCommand bool
}{
{
test: "with no alternate command and ucp active resource update successfully",
exitStatus: "0",
expectedOutput: "context update ucp-active-resource test-context --project projectA --space spaceA succeeded\n",
expectedFailure: false,
},
{
test: "with no alternate command and ucp active resource update unsuccessfully",
exitStatus: "1",
expectedOutput: "context update ucp-active-resource test-context --project projectA --space spaceA failed\n",
expectedFailure: true,
},
{
test: "with alternate command and ucp active resource update successfully",
newCommandExitStatus: "0",
expectedOutput: "newcommand update ucp-active-resource test-context --project projectA --space spaceA succeeded\n",
expectedFailure: false,
enableCustomCommand: true,
},
{
test: "with alternate command and ucp active resource update unsuccessfully",
newCommandExitStatus: "1",
expectedOutput: "newcommand update ucp-active-resource test-context --project projectA --space spaceA failed\n",
expectedFailure: true,
enableCustomCommand: true,
},
}

for _, spec := range tests {
dir, err := os.MkdirTemp("", "tanzu-set-ucp-active-resource-api")
assert.Nil(t, err)
defer os.RemoveAll(dir)
t.Run(spec.test, func(t *testing.T) {
assert := assert.New(t)

// Set up stdout and stderr for our test
r, w, err := os.Pipe()
if err != nil {
t.Error(err)
}
c := make(chan []byte)
go readOutput(t, r, c)
stdout := os.Stdout
stderr := os.Stderr
defer func() {
os.Stdout = stdout
os.Stderr = stderr
}()
os.Stdout = w
os.Stderr = w

cliPath, err := setupFakeCLI(dir, spec.exitStatus, spec.newCommandExitStatus, spec.enableCustomCommand)
assert.Nil(err)
os.Setenv("TANZU_BIN", cliPath)

// Test-1:
// - verify correct combinedOutput string returned as part of the output
// - verify correct string gets printed to default stdout and stderr
err = SetUCPContextActiveResource("test-context", "projectA", "spaceA")
w.Close()
stdoutRecieved := <-c

if spec.expectedFailure {
assert.NotNil(err)
} else {
assert.Nil(err)
}

assert.Equal(spec.expectedOutput, string(stdoutRecieved), "incorrect combinedOutput result")

// Test-2: when external stdout and stderr are provided with WithStdout, WithStderr options,
// verify correct string gets printed to provided custom stdout/stderr
var combinedOutputBuff bytes.Buffer
err = SetUCPContextActiveResource("test-context", "projectA", "spaceA", WithOutputWriter(&combinedOutputBuff), WithErrorWriter(&combinedOutputBuff))
if spec.expectedFailure {
assert.NotNil(err)
} else {
assert.Nil(err)
}
assert.Equal(spec.expectedOutput, combinedOutputBuff.String(), "incorrect combinedOutputBuff result")

os.Unsetenv("TANZU_BIN")
})
}
}

0 comments on commit fe52a9b

Please sign in to comment.