Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/rad/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import (
upgrade_kubernetes "github.com/radius-project/radius/pkg/cli/cmd/upgrade/kubernetes"
version "github.com/radius-project/radius/pkg/cli/cmd/version"
workspace_create "github.com/radius-project/radius/pkg/cli/cmd/workspace/create"
workspace_create_preview "github.com/radius-project/radius/pkg/cli/cmd/workspace/create/preview"
workspace_delete "github.com/radius-project/radius/pkg/cli/cmd/workspace/delete"
workspace_list "github.com/radius-project/radius/pkg/cli/cmd/workspace/list"
workspace_show "github.com/radius-project/radius/pkg/cli/cmd/workspace/show"
Expand Down Expand Up @@ -388,6 +389,8 @@ func initSubCommands() {
envCmd.AddCommand(envUpdateCmd)

workspaceCreateCmd, _ := workspace_create.NewCommand(framework)
previewWorkspaceCreateCmd, _ := workspace_create_preview.NewCommand(framework)
wirePreviewSubcommand(workspaceCreateCmd, previewWorkspaceCreateCmd)
workspaceCmd.AddCommand(workspaceCreateCmd)
Comment thread
nithyatsu marked this conversation as resolved.

workspaceDeleteCmd, _ := workspace_delete.NewCommand(framework)
Expand Down
40 changes: 36 additions & 4 deletions pkg/cli/cmd/workspace/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/radius-project/radius/pkg/cli/kubernetes"
"github.com/radius-project/radius/pkg/cli/output"
"github.com/radius-project/radius/pkg/cli/workspaces"
"github.com/radius-project/radius/pkg/corerp/datamodel"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -70,6 +71,13 @@ rad workspace create kubernetes`,
return cmd, runner
}

// EnvironmentValidator validates that the environment named envName exists in the
// scope of ws, and returns the fully-qualified environment resource ID to be persisted
// on the workspace. mgmtClient is the management client already created for the workspace
// scope; implementations are free to ignore it (e.g. preview implementations that talk
// to the Radius.Core resource provider directly).
type EnvironmentValidator func(ctx context.Context, ws *workspaces.Workspace, mgmtClient clients.ApplicationsManagementClient, envName string) (envID string, err error)

// Runner is the runner implementation for the `rad workspace create` command.
type Runner struct {
ConfigHolder *framework.ConfigHolder
Expand All @@ -80,18 +88,38 @@ type Runner struct {
Output output.Interface
HelmInterface helm.Interface
KubernetesInterface kubernetes.Interface

// EnvironmentValidator is invoked to validate that the requested environment exists
// and to construct the environment resource ID stored on the workspace. When nil,
// the legacy Applications.Core/environments validator is used.
EnvironmentValidator EnvironmentValidator
}

// NewRunner creates a new instance of the `rad workspace create` runner.
func NewRunner(factory framework.Factory) *Runner {
return &Runner{
r := &Runner{
ConnectionFactory: factory.GetConnectionFactory(),
ConfigHolder: factory.GetConfigHolder(),
ConfigFileInterface: factory.GetConfigFileInterface(),
Output: factory.GetOutput(),
HelmInterface: factory.GetHelmInterface(),
KubernetesInterface: factory.GetKubernetesInterface(),
}
r.EnvironmentValidator = ValidateApplicationsCoreEnvironment
return r
}

// ValidateApplicationsCoreEnvironment is the default EnvironmentValidator. It targets
// Applications.Core/environments via the management client.
func ValidateApplicationsCoreEnvironment(ctx context.Context, ws *workspaces.Workspace, mgmtClient clients.ApplicationsManagementClient, envName string) (string, error) {
envID := ws.Scope + "/providers/" + datamodel.EnvironmentResourceType + "/" + envName
if _, err := mgmtClient.GetEnvironment(ctx, envName); err != nil {
if clients.Is404Error(err) {
return "", clierrors.Message("The environment %q does not exist. Run `rad env create` and try again.", envID)
}
return "", clierrors.MessageWithCause(err, "Failed to get environment %q.", envID)
}
return envID, nil
}

// Validate runs validation for the `rad workspace create` command.
Expand Down Expand Up @@ -191,12 +219,16 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error {
if r.Workspace.Scope == "" {
return clierrors.Message("Cannot set environment for workspace with empty scope. Use --group to set a scope.")
}
r.Workspace.Environment = r.Workspace.Scope + "/providers/applications.core/environments/" + env

_, err = client.GetEnvironment(cmd.Context(), env)
validator := r.EnvironmentValidator
if validator == nil {
validator = ValidateApplicationsCoreEnvironment
}
envID, err := validator(cmd.Context(), r.Workspace, client, env)
if err != nil {
return clierrors.Message("The environment %q does not exist. Run `rad env create` try again.", r.Workspace.Environment)
return err
}
r.Workspace.Environment = envID
}

return nil
Expand Down
32 changes: 32 additions & 0 deletions pkg/cli/cmd/workspace/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"testing"

"github.com/radius-project/radius/pkg/cli/clients"
"github.com/radius-project/radius/pkg/cli/framework"
"github.com/radius-project/radius/pkg/cli/helm"
"github.com/radius-project/radius/pkg/cli/output"
Expand Down Expand Up @@ -132,6 +133,37 @@ func Test_Validate(t *testing.T) {

}

func Test_ValidateApplicationsCoreEnvironment(t *testing.T) {
ws := &workspaces.Workspace{Scope: "/planes/radius/local/resourceGroups/rg1"}
wantID := "/planes/radius/local/resourceGroups/rg1/providers/Applications.Core/environments/env1"

t.Run("404 returns user-friendly does-not-exist message", func(t *testing.T) {
ctrl := gomock.NewController(t)
mgmt := clients.NewMockApplicationsManagementClient(ctrl)
mgmt.EXPECT().GetEnvironment(gomock.Any(), "env1").Return(corerp.EnvironmentResource{}, radcli.Create404Error()).Times(1)

id, err := ValidateApplicationsCoreEnvironment(context.Background(), ws, mgmt, "env1")
require.Error(t, err)
require.Empty(t, id)
require.Contains(t, err.Error(), wantID)
require.Contains(t, err.Error(), "does not exist")
require.Contains(t, err.Error(), "rad env create")
})

t.Run("non-404 error is propagated and not masked as not-found", func(t *testing.T) {
ctrl := gomock.NewController(t)
mgmt := clients.NewMockApplicationsManagementClient(ctrl)
mgmt.EXPECT().GetEnvironment(gomock.Any(), "env1").Return(corerp.EnvironmentResource{}, errors.New("connection refused")).Times(1)

id, err := ValidateApplicationsCoreEnvironment(context.Background(), ws, mgmt, "env1")
require.Error(t, err)
require.Empty(t, id)
require.Contains(t, err.Error(), "Failed to get environment")
require.Contains(t, err.Error(), wantID)
require.NotContains(t, err.Error(), "does not exist")
})
}

func Test_Run(t *testing.T) {

t.Run("Workspace Create", func(t *testing.T) {
Expand Down
117 changes: 117 additions & 0 deletions pkg/cli/cmd/workspace/create/preview/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
Copyright 2023 The Radius 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 preview implements the `rad workspace create --preview` command. It reuses
// the runner from the parent create package and only overrides the environment
// validation step so that the workspace is bound to a Radius.Core/environments
// resource (v20250801preview) instead of an Applications.Core/environments resource.
package preview

import (
"context"

"github.com/spf13/cobra"

"github.com/radius-project/radius/pkg/cli/clients"
"github.com/radius-project/radius/pkg/cli/clierrors"
"github.com/radius-project/radius/pkg/cli/cmd"
"github.com/radius-project/radius/pkg/cli/cmd/commonflags"
workspace_create "github.com/radius-project/radius/pkg/cli/cmd/workspace/create"
"github.com/radius-project/radius/pkg/cli/framework"
"github.com/radius-project/radius/pkg/cli/workspaces"
corerpv20250801 "github.com/radius-project/radius/pkg/corerp/api/v20250801preview"
"github.com/radius-project/radius/pkg/corerp/datamodel"
)

// NewCommand creates an instance of the command and runner for the `rad workspace create --preview` command.
//
// The preview command behaves identically to `rad workspace create`, but binds the workspace
// to a Radius.Core/environments resource (v20250801preview) instead of an
// Applications.Core/environments resource.
func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) {
runner := NewRunner(factory)

c := &cobra.Command{
Use: "create [workspaceType] [workspaceName]",
Short: "Create a workspace (preview)",
Long: `Create a workspace bound to a Radius.Core (preview) environment.

Available workspaceTypes: kubernetes

Workspaces allow you to manage multiple Radius platforms and environments using a local configuration file.

Use this command together with environments created via 'rad env create --preview'.`,
Args: workspace_create.ValidateArgs(),
Example: `
# Create a workspace bound to a Radius.Core (preview) environment
rad workspace create kubernetes my-workspace --group my-grp --environment my-env --preview

# Create a workspace using the current kubernetes context as the workspace name
rad workspace create kubernetes --preview`,
RunE: framework.RunCommand(runner),
}

commonflags.AddWorkspaceFlag(c)
commonflags.AddResourceGroupFlag(c)
commonflags.AddEnvironmentNameFlag(c)
c.Flags().BoolP("force", "f", false, "Overwrite existing workspace if present")
c.Flags().StringP("context", "c", "", "the Kubernetes context to use, will use the default if unset")

return c, runner
}

// Runner is the runner implementation for the `rad workspace create --preview` command.
//
// It embeds the legacy create.Runner and overrides only the environment validation
// step. All other workspace-creation behaviour (kube context, install check, group
// lookup, persistence) is inherited unchanged.
type Runner struct {
*workspace_create.Runner

// RadiusCoreClientFactory validates the Radius.Core/environments resource. When nil,
// it is initialized lazily from the workspace connection. Tests may pre-populate it.
RadiusCoreClientFactory *corerpv20250801.ClientFactory
}

// NewRunner creates a new instance of the `rad workspace create --preview` runner.
func NewRunner(factory framework.Factory) *Runner {
base := workspace_create.NewRunner(factory)
r := &Runner{Runner: base}
base.EnvironmentValidator = r.validateRadiusCoreEnvironment
return r
}

// validateRadiusCoreEnvironment validates that the Radius.Core/environments resource
// exists in the workspace scope and returns its fully-qualified resource ID.
func (r *Runner) validateRadiusCoreEnvironment(ctx context.Context, ws *workspaces.Workspace, _ clients.ApplicationsManagementClient, envName string) (string, error) {
envID := ws.Scope + "/providers/" + datamodel.EnvironmentResourceType_v20250801preview + "/" + envName

if r.RadiusCoreClientFactory == nil {
clientFactory, err := cmd.InitializeRadiusCoreClientFactory(ctx, ws, ws.Scope)
if err != nil {
return "", err
}
r.RadiusCoreClientFactory = clientFactory
}

if _, err := r.RadiusCoreClientFactory.NewEnvironmentsClient().Get(ctx, envName, nil); err != nil {
if clients.Is404Error(err) {
return "", clierrors.Message("The environment %q does not exist. Run `rad env create --preview` and try again.", envID)
}
return "", clierrors.MessageWithCause(err, "Failed to get environment %q.", envID)
}
Comment on lines +110 to +115
return envID, nil
}
Loading
Loading