Skip to content
Merged
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
1 change: 1 addition & 0 deletions internal/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func makeChildCommand(root RootCommand, desc map[string]cmdutils.Descriptor) *co
cmd.AddGroup(
&cobra.Group{ID: "admin-essentials", Title: "Admin Essentials Commands:"},
&cobra.Group{ID: "automation-studio", Title: "Automation Studio Commands:"},
&cobra.Group{ID: "agent-projects", Title: "Agent Project Commands:"},
&cobra.Group{ID: "configuration-manager", Title: "Configuration Manager Commands:"},
&cobra.Group{ID: "operations-manager", Title: "Operations Manager Commands:"},
&cobra.Group{ID: "lifecycle-manager", Title: "Lifecycle Manager Commands:"},
Expand Down
18 changes: 18 additions & 0 deletions internal/flags/agent_projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ package flags

import "github.com/spf13/cobra"

// AgentProjectCreateOptions holds options for the `create agent-project` command.
type AgentProjectCreateOptions struct {
Description string
}

func (o *AgentProjectCreateOptions) Flags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.Description, "description", o.Description, "Description for the agent project (optional)")
}

// AgentProjectImportOptions holds options for the `import agent-project` command.
type AgentProjectImportOptions struct {
Members []string
Expand All @@ -23,3 +32,12 @@ func (o *AgentProjectImportOptions) Flags(cmd *cobra.Command) {
type AgentProjectExportOptions struct{}

func (o *AgentProjectExportOptions) Flags(_ *cobra.Command) {}

// AgentProjectCopyOptions holds options for the `copy agent-project` command.
type AgentProjectCopyOptions struct {
Members []string
}

func (o *AgentProjectCopyOptions) Flags(cmd *cobra.Command) {
cmd.Flags().StringArrayVar(&o.Members, "member", o.Members, "Configure one or more agent project members")
}
8 changes: 8 additions & 0 deletions internal/flags/agent_projects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
"github.com/stretchr/testify/assert"
)

func TestAgentProjectCreateOptions_Flags(t *testing.T) {
checkFlags(t, &AgentProjectCreateOptions{}, []string{"description"})
}

func TestAgentProjectImportOptions_Flags(t *testing.T) {
checkFlags(t, &AgentProjectImportOptions{}, []string{"member"})
}
Expand All @@ -21,3 +25,7 @@ func TestAgentProjectExportOptions_Flags(t *testing.T) {
opts.Flags(cmd)
assert.NotNil(t, opts)
}

func TestAgentProjectCopyOptions_Flags(t *testing.T) {
checkFlags(t, &AgentProjectCopyOptions{}, []string{"member"})
}
2 changes: 2 additions & 0 deletions internal/handlers/agent_projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ func NewAgentProjectHandler(rt *Runtime, desc Descriptors) AssetHandler {
runners.NewAgentProjectRunner(rt.GetClient(), rt.GetConfig()),
desc[agentProjectsDescriptor],
&AssetHandlerFlags{
Create: &flags.AgentProjectCreateOptions{},
Import: &flags.AgentProjectImportOptions{},
Export: &flags.AgentProjectExportOptions{},
Copy: &flags.AgentProjectCopyOptions{},
},
)
}
3 changes: 1 addition & 2 deletions internal/handlers/descriptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
transformationsDescriptor = "transformations"
jsonformsDescriptor = "jsonforms"
projectsDescriptor = "projects"
agentProjectsDescriptor = "agent_projects"
analyticTemplatesDescriptor = "analytic_templates"
templatesDescriptor = "templates"

Expand All @@ -49,8 +50,6 @@ const (
configurationParsersDescriptor = "configuration_parsers"
gctreesDescriptor = "gctrees"

agentProjectsDescriptor = "agent_projects"

serverDescriptor = "server"

localAAADescriptor = "localaaa"
Expand Down
81 changes: 81 additions & 0 deletions internal/handlers/descriptors/agent_projects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,87 @@ describe:
# Display details about an agent project called `my-agents`
$ ipctl describe agent-project "my-agents"

create:
use: agent-project <name>
group: agent-projects

description: |
Create a new agent project.

The `create agent-project` command will create a new empty agent project
with the specified name on the server. An optional description can be
provided via the `--description` flag. If an agent project with the same
name already exists, this command will return an error.

example: |
# Create a new agent project
$ ipctl create agent-project "my agent project"

# Create a new agent project with a description
$ ipctl create agent-project "my agent project" --description "A project for my agents"

delete:
use: agent-project <name>
group: agent-projects

description: |
Delete an agent project.

Use the `delete agent-project` command to delete an existing agent project
from the server. If the specified agent project does not exist on the
server, this command will return an error.

example: |
# Delete an agent project named `my agent project`
$ ipctl delete agent-project "my agent project"

copy:
use: agent-project <name> --from <profile> --to <profile>
group: agent-projects

description: |
Copy an agent project from one server to another.

The copy command can be used to copy an agent project from one Itential
Platform server to another.

The `name` argument specifies the agent project name to copy from the
source server to the target. If it does not exist on the source server,
the command will return an error.

The command has two required options, `--from` and `--to`. The `--from`
option specifies the name of the ipctl profile that points to the server
to copy from. The `--to` option specifies the name of the ipctl profile
that points to the server to copy to.

When the copy operation is performed, the destination server is first
checked to see if the agent project already exists. If it does exist,
the copy operation will return an error unless `--replace` is specified.

All member privileges are removed from the project on copy. Use the
`--member` option to set permissions on the destination server.

example: |
# Copy an agent project from `ipctl-dev` to `ipctl-staging`
$ ipctl copy agent-project "my agent project" --from ipctl-dev --to ipctl-staging

# Copy and set the group `admins` as owner
$ ipctl copy agent-project "my agent project" --from ipctl-dev --to ipctl-staging --member type=group,name=admins,access=owner

clear:
use: agent-projects
group: agent-projects

description: |
Delete all configured agent projects.

The `clear agent-projects` command will delete all configured agent projects
from the server. This is a destructive command and cannot be undone.

example: |
# Delete all agent projects from the server
$ ipctl clear agent-projects

import:
use: agent-project <path>
group: flow-agent
Expand Down
171 changes: 171 additions & 0 deletions internal/runners/agent_projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,177 @@ func (r *AgentProjectRunner) Describe(in Request) (*Response, error) {
}, nil
}

//////////////////////////////////////////////////////////////////////////////
// Writer Interface
//

// Create creates a new agent project with the given name and an optional description.
func (r *AgentProjectRunner) Create(in Request) (*Response, error) {
logging.Trace()

name := in.Args[0]
description := in.Options.(*flags.AgentProjectCreateOptions).Description

existing, _ := r.resource.GetByName(name)
if existing != nil {
return nil, fmt.Errorf("agent project %q already exists", name)
}

project, err := r.resource.Create(name, description)
if err != nil {
return nil, err
}

return &Response{
Text: fmt.Sprintf("Successfully created agent project `%s` (%s)", project.Name, project.Id),
Object: project,
}, nil
}

// Delete removes an agent project by name.
func (r *AgentProjectRunner) Delete(in Request) (*Response, error) {
logging.Trace()

project, err := r.resource.GetByName(in.Args[0])
if err != nil {
return nil, err
}

if err := r.resource.Delete(project.Id); err != nil {
return nil, err
}

return &Response{
Text: fmt.Sprintf("Successfully deleted agent project `%s` (%s)", project.Name, project.Id),
}, nil
}

// Clear removes all agent projects from the platform.
func (r *AgentProjectRunner) Clear(in Request) (*Response, error) {
logging.Trace()

projects, err := r.resource.GetAll()
if err != nil {
return nil, err
}

for _, p := range projects {
if err := r.resource.Delete(p.Id); err != nil {
logging.Debug("failed to delete agent project `%s` (%s)", p.Name, p.Id)
return nil, err
}
}

return &Response{
Text: fmt.Sprintf("Deleted %v agent project(s)", len(projects)),
}, nil
}

//////////////////////////////////////////////////////////////////////////////
// Copier Interface
//

// Copy copies an agent project from the current profile to a destination profile.
func (r *AgentProjectRunner) Copy(in Request) (*Response, error) {
logging.Trace()

res, err := Copy(CopyRequest{Request: in, Type: "agent project"}, r)
if err != nil {
return nil, err
}

dstClient, cancel, err := NewClient(in.Common.(*flags.AssetCopyCommon).To, r.config)
if err != nil {
return nil, err
}
defer cancel()

accounts := services.NewAccountService(dstClient)
groups := services.NewGroupService(dstClient)
userSettings := services.NewUserSettingsService(dstClient)

activeUser, err := userSettings.Get()
if err != nil {
return nil, fmt.Errorf("failed to get active user: %w", err)
}

members, err := r.buildAgentProjectMembers(
in.Options.(*flags.AgentProjectCopyOptions).Members,
activeUser.Username,
accounts,
groups,
)
if err != nil {
return nil, err
}

if len(members) > 0 {
projectRes := resources.NewAgentProjectResource(services.NewAgentProjectService(dstClient))
bundle := res.CopyToData.(services.AgentProjectBundle)
if err := projectRes.AddMembers(bundle.Id, members); err != nil {
return nil, fmt.Errorf("failed to add members: %w", err)
}
}

return &Response{
Text: fmt.Sprintf("Successfully copied agent project `%s` from `%s` to `%s`", res.Name, res.From, res.To),
}, nil
}

// CopyFrom exports an agent project bundle from the specified profile.
func (r *AgentProjectRunner) CopyFrom(profile, name string) (any, error) {
logging.Trace()

c, cancel, err := NewClient(profile, r.config)
if err != nil {
return nil, err
}
defer cancel()

projectRes := resources.NewAgentProjectResource(services.NewAgentProjectService(c))

project, err := projectRes.GetByName(name)
if err != nil {
return nil, err
}

bundle, err := projectRes.Export(project.Id)
if err != nil {
return nil, err
}

return *bundle, nil
}

// CopyTo imports an agent project bundle to the specified profile.
func (r *AgentProjectRunner) CopyTo(profile string, in any, replace bool) (any, error) {
logging.Trace()

c, cancel, err := NewClient(profile, r.config)
if err != nil {
return nil, err
}
defer cancel()

projectRes := resources.NewAgentProjectResource(services.NewAgentProjectService(c))

bundle, ok := in.(services.AgentProjectBundle)
if !ok {
return nil, fmt.Errorf("expected services.AgentProjectBundle, got %T", in)
}

if exists, _ := projectRes.GetByName(bundle.Name); exists != nil {
if !replace {
return nil, fmt.Errorf("agent project %q exists on the destination server, use --replace to overwrite", bundle.Name)
}
if err := projectRes.Delete(exists.Id); err != nil {
return nil, fmt.Errorf("failed to delete existing agent project: %w", err)
}
}

return projectRes.Import(bundle, "keep-both")
}

//////////////////////////////////////////////////////////////////////////////
// Importer Interface
//
Expand Down
6 changes: 5 additions & 1 deletion pkg/resources/agent_projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ func (r *AgentProjectResource) Import(bundle services.AgentProjectBundle, confli
return r.service.Import(bundle, conflictMode)
}

// Create creates a new agent project with the specified name and description.
func (r *AgentProjectResource) Create(name string, description string) (*services.AgentProject, error) {
return r.service.Create(name, description)
}

// Delete removes an agent project by its identifier.
// This is a pass-through to the service layer for pure API access.
func (r *AgentProjectResource) Delete(id string) error {
return r.service.Delete(id)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/resources/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type AgentProjectResourcer interface {
GetAll() ([]services.AgentProject, error)
Get(id string) (*services.AgentProject, error)
GetByName(name string) (*services.AgentProject, error)
Create(name string, description string) (*services.AgentProject, error)
Export(id string) (*services.AgentProjectBundle, error)
Import(bundle services.AgentProjectBundle, conflictMode string) (*services.AgentProjectBundle, error)
AddMembers(projectId string, members []services.AgentProjectMember) error
Expand Down
Loading