diff --git a/cmd/harbor/root/project/cmd.go b/cmd/harbor/root/project/cmd.go index 5942e2b69..a64d802fc 100644 --- a/cmd/harbor/root/project/cmd.go +++ b/cmd/harbor/root/project/cmd.go @@ -35,6 +35,7 @@ func Project() *cobra.Command { SearchProjectCommand(), Robot(), Member(), + Preheat(), ) return cmd diff --git a/cmd/harbor/root/project/preheat.go b/cmd/harbor/root/project/preheat.go new file mode 100644 index 000000000..4558fe0b4 --- /dev/null +++ b/cmd/harbor/root/project/preheat.go @@ -0,0 +1,37 @@ +// Copyright Project Harbor 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 project + +import ( + "github.com/goharbor/harbor-cli/cmd/harbor/root/project/preheat" + "github.com/spf13/cobra" +) + +func Preheat() *cobra.Command { + cmd := &cobra.Command{ + Use: "preheat", + Aliases: []string{"p2p"}, + Short: "Manage project preheat resources", + Long: "Manage project-scoped P2P preheat policies, executions, and tasks in Harbor", + Example: ` harbor project preheat policy list [PROJECT_NAME] + harbor project preheat policy list [PROJECT_ID] --id + harbor project preheat policy create -f [CONFIG_FILE] + harbor project preheat policy start [PROJECT_NAME] [POLICY_NAME]`, + } + cmd.AddCommand( + preheat.PolicyCommand(), + ) + + return cmd +} diff --git a/cmd/harbor/root/project/preheat/policy.go b/cmd/harbor/root/project/preheat/policy.go new file mode 100644 index 000000000..c240591b9 --- /dev/null +++ b/cmd/harbor/root/project/preheat/policy.go @@ -0,0 +1,44 @@ +// Copyright Project Harbor 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 preheat + +import ( + "github.com/goharbor/harbor-cli/cmd/harbor/root/project/preheat/policy" + "github.com/spf13/cobra" +) + +func PolicyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "policy", + Aliases: []string{"pol"}, + Short: "Manage preheat policies", + Long: "Manage P2P preheat policies under a project", + Example: ` harbor project preheat policy list [PROJECT_NAME] + harbor project preheat policy list [PROJECT_ID] --id + harbor project preheat policy create -f [CONFIG_FILE] + harbor project preheat policy view [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy start [PROJECT_NAME] [POLICY_NAME]`, + } + + cmd.AddCommand( + policy.ListPolicyCommand(), + policy.ViewPolicyCommand(), + policy.CreatePolicyCommand(), + policy.UpdatePolicyCommand(), + policy.DeletePolicyCommand(), + policy.StartPolicyCommand(), + ) + + return cmd +} diff --git a/cmd/harbor/root/project/preheat/policy/create.go b/cmd/harbor/root/project/preheat/policy/create.go new file mode 100644 index 000000000..b7ea4ac01 --- /dev/null +++ b/cmd/harbor/root/project/preheat/policy/create.go @@ -0,0 +1,194 @@ +// Copyright Project Harbor 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 policy + +import ( + "encoding/json" + "fmt" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/api" + config "github.com/goharbor/harbor-cli/pkg/config/preheat" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/preheat/policy/create" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func CreatePolicyCommand() *cobra.Command { + var isID bool + var configFile string + var err error + var opts *create.CreateView + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a preheat policy", + Long: "Create a new P2P preheat policy under a project", + Example: ` harbor project preheat policy create [PROJECT_NAME] + harbor project preheat policy create [PROJECT_ID] --id + harbor project preheat policy create -f [CONFIG_FILE]`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + log.Debug("Starting preheat policy create command") + + if configFile != "" { + if len(args) > 0 { + return fmt.Errorf("arguments are not supported with --policy-config-file") + } + + log.Debugf("Loading preheat policy configuration from file: %s", configFile) + opts, err = config.LoadConfigFromFile(configFile) + if err != nil { + return fmt.Errorf("failed to load preheat policy configuration: %v", err) + } + } else { + opts = &create.CreateView{ + Enabled: true, + } + + if isID && len(args) == 0 { + return fmt.Errorf("project ID must be provided when using --id") + } + + if len(args) > 0 { + log.Debugf("Project name provided: %s", args[0]) + opts.ProjectName = args[0] + } else { + log.Debug("No project name provided, prompting user") + opts.ProjectName, err = prompt.GetProjectNameFromUser() + if err != nil { + return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + if isID { + project, err := api.GetProject(opts.ProjectName, true) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return fmt.Errorf("project %s does not exist", opts.ProjectName) + } + return fmt.Errorf("failed to get project: %v", utils.ParseHarborErrorMsg(err)) + } + opts.ProjectName = project.Payload.Name + } + } + + _, err = api.GetProject(opts.ProjectName, false) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return fmt.Errorf("project %s does not exist. If you entered a project ID, use --id", opts.ProjectName) + } + return fmt.Errorf("failed to verify project: %v", utils.ParseHarborErrorMsg(err)) + } + + log.Debug("Fetching available providers...") + providers, err := api.ListProvidersUnderProject(opts.ProjectName) + if err != nil { + return fmt.Errorf("failed to list providers: %v", utils.ParseHarborErrorMsg(err)) + } + + if len(providers) == 0 { + return fmt.Errorf("no P2P provider instances available for project '%s'. Please create a provider instance first", opts.ProjectName) + } + + if configFile == "" { + create.CreatePreheatPolicyView(opts, providers) + } + + providerID, err := resolveProviderID(providers, opts.ProviderName, opts.ProjectName) + if err != nil { + return err + } + + policy, err := ConvertToPolicy(opts, providerID) + if err != nil { + return err + } + + log.Debug("Creating preheat policy...") + response, err := api.CreatePreheatPolicy(opts.ProjectName, policy) + if err != nil { + if utils.ParseHarborErrorCode(err) == "409" { + return fmt.Errorf("preheat policy '%s' already exists in project '%s'", opts.Name, opts.ProjectName) + } + return fmt.Errorf("failed to create preheat policy: %v", utils.ParseHarborErrorMsg(err)) + } + + fmt.Println("Preheat policy created successfully with ID:", response.Location) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&isID, "id", false, "Use project id instead of name") + flags.StringVarP(&configFile, "policy-config-file", "f", "", "YAML/JSON file with preheat policy configuration") + + return cmd +} + +func resolveProviderID(providers []*models.ProviderUnderProject, providerName, projectName string) (int64, error) { + for _, provider := range providers { + if provider.Provider == providerName && provider.Enabled { + return provider.ID, nil + } + } + + return 0, fmt.Errorf("provider '%s' not found or not enabled for project '%s'", providerName, projectName) +} + +func ConvertToPolicy(view *create.CreateView, providerID int64) (*models.PreheatPolicy, error) { + type filter struct { + Type string `json:"type"` + Value string `json:"value"` + } + filters := []filter{ + {Type: "repository", Value: view.RepositoryFilter}, + {Type: "tag", Value: view.TagFilter}, + } + if view.LabelFilter != "" { + filters = append(filters, filter{Type: "label", Value: view.LabelFilter}) + } + filtersJSON, err := json.Marshal(filters) + if err != nil { + return nil, fmt.Errorf("failed to marshal filters: %v", err) + } + + type triggerSetting struct { + Cron string `json:"cron"` + } + type trigger struct { + Type string `json:"type"` + TriggerSetting *triggerSetting `json:"trigger_setting,omitempty"` + } + t := trigger{Type: view.TriggerType} + if view.TriggerType == "scheduled" { + t.TriggerSetting = &triggerSetting{Cron: view.CronString} + } + triggerJSON, err := json.Marshal(t) + if err != nil { + return nil, fmt.Errorf("failed to marshal trigger: %v", err) + } + + return &models.PreheatPolicy{ + Name: view.Name, + Description: view.Description, + ProviderID: providerID, + ProviderName: view.ProviderName, + Filters: string(filtersJSON), + Trigger: string(triggerJSON), + Enabled: view.Enabled, + }, nil +} diff --git a/cmd/harbor/root/project/preheat/policy/delete.go b/cmd/harbor/root/project/preheat/policy/delete.go new file mode 100644 index 000000000..004826db0 --- /dev/null +++ b/cmd/harbor/root/project/preheat/policy/delete.go @@ -0,0 +1,92 @@ +// Copyright Project Harbor 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 policy + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/utils" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func DeletePolicyCommand() *cobra.Command { + var isID bool + + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a preheat policy", + Long: "Delete a specific P2P preheat policy under a project", + Example: ` harbor project preheat policy delete [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy delete [PROJECT_ID] [POLICY_NAME] --id`, + Args: cobra.MaximumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + var projectName, policyName string + + if isID && len(args) == 0 { + return fmt.Errorf("project ID must be provided when using --id") + } + + if len(args) >= 1 { + log.Debugf("Project name provided: %s", args[0]) + projectName = args[0] + } else { + log.Debug("No project name provided, prompting user") + projectName, err = prompt.GetProjectNameFromUser() + if err != nil { + return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + if isID { + project, err := api.GetProject(projectName, true) + if err != nil { + return fmt.Errorf("failed to resolve project ID: %v", utils.ParseHarborErrorMsg(err)) + } + projectName = project.Payload.Name + } + + if len(args) >= 2 { + log.Debugf("Policy name provided: %s", args[1]) + policyName = args[1] + } else { + log.Debug("No policy name provided, prompting user") + policyName, err = prompt.GetPreheatPolicyNameFromUser(projectName) + if err != nil { + return fmt.Errorf("failed to get policy name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + log.Debug("Deleting preheat policy...") + err = api.DeletePreheatPolicy(projectName, policyName) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return fmt.Errorf("preheat policy %s not found in project %s", policyName, projectName) + } + return fmt.Errorf("failed to delete preheat policy: %v", utils.ParseHarborErrorMsg(err)) + } + + fmt.Printf("Preheat policy '%s' deleted successfully from project '%s'\n", policyName, projectName) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&isID, "id", false, "Delete preheat policy by project id") + + return cmd +} diff --git a/cmd/harbor/root/project/preheat/policy/list.go b/cmd/harbor/root/project/preheat/policy/list.go new file mode 100644 index 000000000..1f332e957 --- /dev/null +++ b/cmd/harbor/root/project/preheat/policy/list.go @@ -0,0 +1,95 @@ +// Copyright Project Harbor 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 policy + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/preheat/policy/list" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ListPolicyCommand() *cobra.Command { + var opts api.ListFlags + var isID bool + + cmd := &cobra.Command{ + Use: "list", + Short: "List preheat policies under a project", + Long: "List project-scoped P2P preheat policies in Harbor", + Example: ` harbor project preheat policy list [PROJECT_NAME] + harbor project preheat policy list [PROJECT_ID] --id`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + var projectName string + + if opts.Page < 1 { + return fmt.Errorf("page number must be greater than or equal to 1") + } + if opts.PageSize <= 0 || opts.PageSize > 100 { + return fmt.Errorf("page size must be greater than 0 and less than or equal to 100") + } + + if isID && len(args) == 0 { + return fmt.Errorf("project ID must be provided when using --id") + } + + if len(args) > 0 { + log.Debugf("Project name provided: %s", args[0]) + projectName = args[0] + } else { + log.Debug("No project name provided, prompting user") + projectName, err = prompt.GetProjectNameFromUser() + if err != nil { + return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + log.Debug("Fetching preheat policies...") + resp, err := api.ListPreheatPolicies(projectName, isID, opts) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return fmt.Errorf("project %s does not exist", projectName) + } + return fmt.Errorf("failed to list preheat policies: %v", utils.ParseHarborErrorMsg(err)) + } + + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(resp.Payload, FormatFlag) + if err != nil { + return err + } + } else { + list.ListPolicies(resp.Payload) + } + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&isID, "id", false, "Get preheat policies by project id") + flags.Int64VarP(&opts.Page, "page", "", 1, "Page number") + flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page") + flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources") + flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the resource list in ascending or descending order") + + return cmd +} diff --git a/cmd/harbor/root/project/preheat/policy/start.go b/cmd/harbor/root/project/preheat/policy/start.go new file mode 100644 index 000000000..224a8921a --- /dev/null +++ b/cmd/harbor/root/project/preheat/policy/start.go @@ -0,0 +1,92 @@ +// Copyright Project Harbor 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 policy + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/utils" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func StartPolicyCommand() *cobra.Command { + var isID bool + + cmd := &cobra.Command{ + Use: "start", + Short: "Manually trigger a preheat policy", + Long: "Manually trigger a specific P2P preheat policy under a project", + Example: ` harbor project preheat policy start [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy start [PROJECT_ID] [POLICY_NAME] --id`, + Args: cobra.MaximumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + var projectName, policyName string + + if isID && len(args) == 0 { + return fmt.Errorf("project ID must be provided when using --id") + } + + if len(args) >= 1 { + log.Debugf("Project name provided: %s", args[0]) + projectName = args[0] + } else { + log.Debug("No project name provided, prompting user") + projectName, err = prompt.GetProjectNameFromUser() + if err != nil { + return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + if isID { + project, err := api.GetProject(projectName, true) + if err != nil { + return fmt.Errorf("failed to resolve project ID: %v", utils.ParseHarborErrorMsg(err)) + } + projectName = project.Payload.Name + } + + if len(args) >= 2 { + log.Debugf("Policy name provided: %s", args[1]) + policyName = args[1] + } else { + log.Debug("No policy name provided, prompting user") + policyName, err = prompt.GetPreheatPolicyNameFromUser(projectName) + if err != nil { + return fmt.Errorf("failed to get policy name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + log.Debug("Manually triggering preheat policy...") + location, err := api.StartPreheatPolicy(projectName, policyName) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return fmt.Errorf("preheat policy %s not found in project %s", policyName, projectName) + } + return fmt.Errorf("failed to manually trigger preheat policy: %v", utils.ParseHarborErrorMsg(err)) + } + + fmt.Printf("Preheat policy '%s' manually triggered successfully from project '%s'\nExecution location: %s\n", policyName, projectName, location) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&isID, "id", false, "Manually trigger preheat policy by project id") + + return cmd +} diff --git a/cmd/harbor/root/project/preheat/policy/update.go b/cmd/harbor/root/project/preheat/policy/update.go new file mode 100644 index 000000000..abdb21da0 --- /dev/null +++ b/cmd/harbor/root/project/preheat/policy/update.go @@ -0,0 +1,184 @@ +// Copyright Project Harbor 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 policy + +import ( + "encoding/json" + "fmt" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/preheat/policy/create" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func UpdatePolicyCommand() *cobra.Command { + var isID bool + + cmd := &cobra.Command{ + Use: "update", + Short: "Update a preheat policy", + Long: "Update an existing P2P preheat policy under a project", + Example: ` harbor project preheat policy update [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy update [PROJECT_ID] [POLICY_NAME] --id`, + Args: cobra.MaximumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + var projectName, policyName string + + if isID && len(args) == 0 { + return fmt.Errorf("project ID must be provided when using --id") + } + + if len(args) >= 1 { + log.Debugf("Project name provided: %s", args[0]) + projectName = args[0] + } else { + log.Debug("No project name provided, prompting user") + projectName, err = prompt.GetProjectNameFromUser() + if err != nil { + return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + if isID { + project, err := api.GetProject(projectName, true) + if err != nil { + return fmt.Errorf("failed to get project: %v", utils.ParseHarborErrorMsg(err)) + } + projectName = project.Payload.Name + } + + if len(args) >= 2 { + log.Debugf("Policy name provided: %s", args[1]) + policyName = args[1] + } else { + log.Debug("No policy name provided, prompting user") + policyName, err = prompt.GetPreheatPolicyNameFromUser(projectName) + if err != nil { + return fmt.Errorf("failed to get policy name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + log.Debug("Fetching preheat policy...") + existingPolicy, err := api.GetPreheatPolicy(projectName, policyName) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return fmt.Errorf("preheat policy %s not found in project %s", policyName, projectName) + } + return fmt.Errorf("failed to get preheat policy: %v", utils.ParseHarborErrorMsg(err)) + } + + log.Debug("Fetching available providers...") + providers, err := api.ListProvidersUnderProject(projectName) + if err != nil { + return fmt.Errorf("failed to list providers: %v", utils.ParseHarborErrorMsg(err)) + } + + if len(providers) == 0 { + return fmt.Errorf("no P2P provider instances available for project '%s'. Please create a provider instance first", projectName) + } + + opts := policyToCreateView(existingPolicy.Payload, providers) + create.CreatePreheatPolicyView(opts, providers) + + providerID, err := resolveProviderID(providers, opts.ProviderName, projectName) + if err != nil { + return err + } + + policy, err := ConvertToPolicy(opts, providerID) + if err != nil { + return err + } + policy.ID = existingPolicy.Payload.ID + policy.ProjectID = existingPolicy.Payload.ProjectID + policy.CreationTime = existingPolicy.Payload.CreationTime + policy.ExtraAttrs = existingPolicy.Payload.ExtraAttrs + + log.Debug("Updating preheat policy...") + _, err = api.UpdatePreheatPolicy(projectName, policyName, policy) + if err != nil { + if utils.ParseHarborErrorCode(err) == "409" { + return fmt.Errorf("preheat policy '%s' already exists in project '%s'", opts.Name, projectName) + } + return fmt.Errorf("failed to update preheat policy: %v", utils.ParseHarborErrorMsg(err)) + } + + fmt.Printf("Preheat policy '%s' updated successfully in project '%s'\n", opts.Name, projectName) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&isID, "id", false, "Use project id instead of name") + + return cmd +} + +func policyToCreateView(policy *models.PreheatPolicy, providers []*models.ProviderUnderProject) *create.CreateView { + view := &create.CreateView{ + Name: policy.Name, + Description: policy.Description, + Enabled: policy.Enabled, + ProviderName: policy.ProviderName, + TriggerType: "manual", + } + + for _, provider := range providers { + if provider.ID == policy.ProviderID { + view.ProviderName = provider.Provider + break + } + } + + var filters []struct { + Type string `json:"type"` + Value string `json:"value"` + } + _ = json.Unmarshal([]byte(policy.Filters), &filters) + for _, filter := range filters { + switch filter.Type { + case "repository": + view.RepositoryFilter = filter.Value + case "tag": + view.TagFilter = filter.Value + case "label": + view.LabelFilter = filter.Value + } + } + + type triggerSetting struct { + Cron string `json:"cron"` + } + var trigger struct { + Type string `json:"type"` + TriggerSetting *triggerSetting `json:"trigger_setting"` + TriggerSettings *triggerSetting `json:"trigger_settings"` + } + _ = json.Unmarshal([]byte(policy.Trigger), &trigger) + if trigger.Type != "" { + view.TriggerType = trigger.Type + } + if trigger.TriggerSetting != nil { + view.CronString = trigger.TriggerSetting.Cron + } else if trigger.TriggerSettings != nil { + view.CronString = trigger.TriggerSettings.Cron + } + + return view +} diff --git a/cmd/harbor/root/project/preheat/policy/view.go b/cmd/harbor/root/project/preheat/policy/view.go new file mode 100644 index 000000000..c9972bb56 --- /dev/null +++ b/cmd/harbor/root/project/preheat/policy/view.go @@ -0,0 +1,102 @@ +// Copyright Project Harbor 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 policy + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/utils" + view "github.com/goharbor/harbor-cli/pkg/views/preheat/policy/view" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ViewPolicyCommand() *cobra.Command { + var isID bool + + cmd := &cobra.Command{ + Use: "view", + Short: "View details of a preheat policy", + Long: "Get details of a specific P2P preheat policy under a project", + Example: ` harbor project preheat policy view [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy view [PROJECT_ID] [POLICY_NAME] --id`, + Args: cobra.MaximumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + var projectName, policyName string + + if isID && len(args) == 0 { + return fmt.Errorf("project ID must be provided when using --id") + } + + if len(args) >= 1 { + log.Debugf("Project name provided: %s", args[0]) + projectName = args[0] + } else { + log.Debug("No project name provided, prompting user") + projectName, err = prompt.GetProjectNameFromUser() + if err != nil { + return fmt.Errorf("failed to get project name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + if isID { + project, err := api.GetProject(projectName, true) + if err != nil { + return fmt.Errorf("failed to resolve project ID: %v", utils.ParseHarborErrorMsg(err)) + } + projectName = project.Payload.Name + } + + if len(args) >= 2 { + log.Debugf("Policy name provided: %s", args[1]) + policyName = args[1] + } else { + log.Debug("No policy name provided, prompting user") + policyName, err = prompt.GetPreheatPolicyNameFromUser(projectName) + if err != nil { + return fmt.Errorf("failed to get policy name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + log.Debug("Fetching preheat policy...") + resp, err := api.GetPreheatPolicy(projectName, policyName) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return fmt.Errorf("preheat policy %s not found in project %s", policyName, projectName) + } + return fmt.Errorf("failed to get preheat policy: %v", utils.ParseHarborErrorMsg(err)) + } + + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(resp.Payload, FormatFlag) + if err != nil { + return err + } + } else { + view.ViewPolicy(resp.Payload) + } + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&isID, "id", false, "Get preheat policy by project id") + + return cmd +} diff --git a/doc/cli-docs/harbor-project-preheat-policy-create.md b/doc/cli-docs/harbor-project-preheat-policy-create.md new file mode 100644 index 000000000..b15ed161e --- /dev/null +++ b/doc/cli-docs/harbor-project-preheat-policy-create.md @@ -0,0 +1,46 @@ +--- +title: harbor project preheat policy create +weight: 50 +--- +## harbor project preheat policy create + +### Description + +##### Create a preheat policy + +### Synopsis + +Create a new P2P preheat policy under a project + +```sh +harbor project preheat policy create [flags] +``` + +### Examples + +```sh + harbor project preheat policy create [PROJECT_NAME] + harbor project preheat policy create [PROJECT_ID] --id + harbor project preheat policy create -f [CONFIG_FILE] +``` + +### Options + +```sh + -h, --help help for create + --id Use project id instead of name + -f, --policy-config-file string YAML/JSON file with preheat policy configuration +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor project preheat policy](harbor-project-preheat-policy.md) - Manage preheat policies + diff --git a/doc/cli-docs/harbor-project-preheat-policy-delete.md b/doc/cli-docs/harbor-project-preheat-policy-delete.md new file mode 100644 index 000000000..a58b1cb94 --- /dev/null +++ b/doc/cli-docs/harbor-project-preheat-policy-delete.md @@ -0,0 +1,44 @@ +--- +title: harbor project preheat policy delete +weight: 20 +--- +## harbor project preheat policy delete + +### Description + +##### Delete a preheat policy + +### Synopsis + +Delete a specific P2P preheat policy under a project + +```sh +harbor project preheat policy delete [flags] +``` + +### Examples + +```sh + harbor project preheat policy delete [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy delete [PROJECT_ID] [POLICY_NAME] --id +``` + +### Options + +```sh + -h, --help help for delete + --id Delete preheat policy by project id +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor project preheat policy](harbor-project-preheat-policy.md) - Manage preheat policies + diff --git a/doc/cli-docs/harbor-project-preheat-policy-list.md b/doc/cli-docs/harbor-project-preheat-policy-list.md new file mode 100644 index 000000000..78330b103 --- /dev/null +++ b/doc/cli-docs/harbor-project-preheat-policy-list.md @@ -0,0 +1,48 @@ +--- +title: harbor project preheat policy list +weight: 20 +--- +## harbor project preheat policy list + +### Description + +##### List preheat policies under a project + +### Synopsis + +List project-scoped P2P preheat policies in Harbor + +```sh +harbor project preheat policy list [flags] +``` + +### Examples + +```sh + harbor project preheat policy list [PROJECT_NAME] + harbor project preheat policy list [PROJECT_ID] --id +``` + +### Options + +```sh + -h, --help help for list + --id Get preheat policies by project id + --page int Page number (default 1) + --page-size int Size of per page (default 10) + -q, --query string Query string to query resources + --sort string Sort the resource list in ascending or descending order +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor project preheat policy](harbor-project-preheat-policy.md) - Manage preheat policies + diff --git a/doc/cli-docs/harbor-project-preheat-policy-start.md b/doc/cli-docs/harbor-project-preheat-policy-start.md new file mode 100644 index 000000000..0296452e0 --- /dev/null +++ b/doc/cli-docs/harbor-project-preheat-policy-start.md @@ -0,0 +1,44 @@ +--- +title: harbor project preheat policy start +weight: 55 +--- +## harbor project preheat policy start + +### Description + +##### Manually trigger a preheat policy + +### Synopsis + +Manually trigger a specific P2P preheat policy under a project + +```sh +harbor project preheat policy start [flags] +``` + +### Examples + +```sh + harbor project preheat policy start [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy start [PROJECT_ID] [POLICY_NAME] --id +``` + +### Options + +```sh + -h, --help help for start + --id Manually trigger preheat policy by project id +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor project preheat policy](harbor-project-preheat-policy.md) - Manage preheat policies + diff --git a/doc/cli-docs/harbor-project-preheat-policy-update.md b/doc/cli-docs/harbor-project-preheat-policy-update.md new file mode 100644 index 000000000..ef5ef0933 --- /dev/null +++ b/doc/cli-docs/harbor-project-preheat-policy-update.md @@ -0,0 +1,44 @@ +--- +title: harbor project preheat policy update +weight: 25 +--- +## harbor project preheat policy update + +### Description + +##### Update a preheat policy + +### Synopsis + +Update an existing P2P preheat policy under a project + +```sh +harbor project preheat policy update [flags] +``` + +### Examples + +```sh + harbor project preheat policy update [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy update [PROJECT_ID] [POLICY_NAME] --id +``` + +### Options + +```sh + -h, --help help for update + --id Use project id instead of name +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor project preheat policy](harbor-project-preheat-policy.md) - Manage preheat policies + diff --git a/doc/cli-docs/harbor-project-preheat-policy-view.md b/doc/cli-docs/harbor-project-preheat-policy-view.md new file mode 100644 index 000000000..867db83b7 --- /dev/null +++ b/doc/cli-docs/harbor-project-preheat-policy-view.md @@ -0,0 +1,44 @@ +--- +title: harbor project preheat policy view +weight: 30 +--- +## harbor project preheat policy view + +### Description + +##### View details of a preheat policy + +### Synopsis + +Get details of a specific P2P preheat policy under a project + +```sh +harbor project preheat policy view [flags] +``` + +### Examples + +```sh + harbor project preheat policy view [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy view [PROJECT_ID] [POLICY_NAME] --id +``` + +### Options + +```sh + -h, --help help for view + --id Get preheat policy by project id +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor project preheat policy](harbor-project-preheat-policy.md) - Manage preheat policies + diff --git a/doc/cli-docs/harbor-project-preheat-policy.md b/doc/cli-docs/harbor-project-preheat-policy.md new file mode 100644 index 000000000..e0c3718c0 --- /dev/null +++ b/doc/cli-docs/harbor-project-preheat-policy.md @@ -0,0 +1,48 @@ +--- +title: harbor project preheat policy +weight: 90 +--- +## harbor project preheat policy + +### Description + +##### Manage preheat policies + +### Synopsis + +Manage P2P preheat policies under a project + +### Examples + +```sh + harbor project preheat policy list [PROJECT_NAME] + harbor project preheat policy list [PROJECT_ID] --id + harbor project preheat policy create -f [CONFIG_FILE] + harbor project preheat policy view [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy start [PROJECT_NAME] [POLICY_NAME] +``` + +### Options + +```sh + -h, --help help for policy +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor project preheat](harbor-project-preheat.md) - Manage project preheat resources +* [harbor project preheat policy create](harbor-project-preheat-policy-create.md) - Create a preheat policy +* [harbor project preheat policy delete](harbor-project-preheat-policy-delete.md) - Delete a preheat policy +* [harbor project preheat policy list](harbor-project-preheat-policy-list.md) - List preheat policies under a project +* [harbor project preheat policy start](harbor-project-preheat-policy-start.md) - Manually trigger a preheat policy +* [harbor project preheat policy update](harbor-project-preheat-policy-update.md) - Update a preheat policy +* [harbor project preheat policy view](harbor-project-preheat-policy-view.md) - View details of a preheat policy + diff --git a/doc/cli-docs/harbor-project-preheat.md b/doc/cli-docs/harbor-project-preheat.md new file mode 100644 index 000000000..fc9d16d91 --- /dev/null +++ b/doc/cli-docs/harbor-project-preheat.md @@ -0,0 +1,42 @@ +--- +title: harbor project preheat +weight: 15 +--- +## harbor project preheat + +### Description + +##### Manage project preheat resources + +### Synopsis + +Manage project-scoped P2P preheat policies, executions, and tasks in Harbor + +### Examples + +```sh + harbor project preheat policy list [PROJECT_NAME] + harbor project preheat policy list [PROJECT_ID] --id + harbor project preheat policy create -f [CONFIG_FILE] + harbor project preheat policy start [PROJECT_NAME] [POLICY_NAME] +``` + +### Options + +```sh + -h, --help help for preheat +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor project](harbor-project.md) - Manage projects and assign resources to them +* [harbor project preheat policy](harbor-project-preheat-policy.md) - Manage preheat policies + diff --git a/doc/cli-docs/harbor-project.md b/doc/cli-docs/harbor-project.md index 70a414107..1fcfaaef5 100644 --- a/doc/cli-docs/harbor-project.md +++ b/doc/cli-docs/harbor-project.md @@ -41,6 +41,7 @@ Manage projects in Harbor * [harbor project list](harbor-project-list.md) - List projects * [harbor project logs](harbor-project-logs.md) - get project logs * [harbor project member](harbor-project-member.md) - Manage members in a Project +* [harbor project preheat](harbor-project-preheat.md) - Manage project preheat resources * [harbor project robot](harbor-project-robot.md) - Manage robot accounts * [harbor project search](harbor-project-search.md) - search project based on their names * [harbor project view](harbor-project-view.md) - get project by name or id diff --git a/doc/man-docs/man1/harbor-project-preheat-policy-create.1 b/doc/man-docs/man1/harbor-project-preheat-policy-create.1 new file mode 100644 index 000000000..711432dcb --- /dev/null +++ b/doc/man-docs/man1/harbor-project-preheat-policy-create.1 @@ -0,0 +1,51 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-project-preheat-policy-create - Create a preheat policy + + +.SH SYNOPSIS +\fBharbor project preheat policy create [flags]\fP + + +.SH DESCRIPTION +Create a new P2P preheat policy under a project + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for create + +.PP +\fB--id\fP[=false] + Use project id instead of name + +.PP +\fB-f\fP, \fB--policy-config-file\fP="" + YAML/JSON file with preheat policy configuration + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor project preheat policy create [PROJECT_NAME] + harbor project preheat policy create [PROJECT_ID] --id + harbor project preheat policy create -f [CONFIG_FILE] +.EE + + +.SH SEE ALSO +\fBharbor-project-preheat-policy(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-project-preheat-policy-delete.1 b/doc/man-docs/man1/harbor-project-preheat-policy-delete.1 new file mode 100644 index 000000000..3df9be2c8 --- /dev/null +++ b/doc/man-docs/man1/harbor-project-preheat-policy-delete.1 @@ -0,0 +1,46 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-project-preheat-policy-delete - Delete a preheat policy + + +.SH SYNOPSIS +\fBharbor project preheat policy delete [flags]\fP + + +.SH DESCRIPTION +Delete a specific P2P preheat policy under a project + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for delete + +.PP +\fB--id\fP[=false] + Delete preheat policy by project id + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor project preheat policy delete [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy delete [PROJECT_ID] [POLICY_NAME] --id +.EE + + +.SH SEE ALSO +\fBharbor-project-preheat-policy(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-project-preheat-policy-list.1 b/doc/man-docs/man1/harbor-project-preheat-policy-list.1 new file mode 100644 index 000000000..f1ad1f079 --- /dev/null +++ b/doc/man-docs/man1/harbor-project-preheat-policy-list.1 @@ -0,0 +1,62 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-project-preheat-policy-list - List preheat policies under a project + + +.SH SYNOPSIS +\fBharbor project preheat policy list [flags]\fP + + +.SH DESCRIPTION +List project-scoped P2P preheat policies in Harbor + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for list + +.PP +\fB--id\fP[=false] + Get preheat policies by project id + +.PP +\fB--page\fP=1 + Page number + +.PP +\fB--page-size\fP=10 + Size of per page + +.PP +\fB-q\fP, \fB--query\fP="" + Query string to query resources + +.PP +\fB--sort\fP="" + Sort the resource list in ascending or descending order + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor project preheat policy list [PROJECT_NAME] + harbor project preheat policy list [PROJECT_ID] --id +.EE + + +.SH SEE ALSO +\fBharbor-project-preheat-policy(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-project-preheat-policy-start.1 b/doc/man-docs/man1/harbor-project-preheat-policy-start.1 new file mode 100644 index 000000000..995a28b92 --- /dev/null +++ b/doc/man-docs/man1/harbor-project-preheat-policy-start.1 @@ -0,0 +1,46 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-project-preheat-policy-start - Manually trigger a preheat policy + + +.SH SYNOPSIS +\fBharbor project preheat policy start [flags]\fP + + +.SH DESCRIPTION +Manually trigger a specific P2P preheat policy under a project + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for start + +.PP +\fB--id\fP[=false] + Manually trigger preheat policy by project id + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor project preheat policy start [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy start [PROJECT_ID] [POLICY_NAME] --id +.EE + + +.SH SEE ALSO +\fBharbor-project-preheat-policy(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-project-preheat-policy-update.1 b/doc/man-docs/man1/harbor-project-preheat-policy-update.1 new file mode 100644 index 000000000..2e8f32242 --- /dev/null +++ b/doc/man-docs/man1/harbor-project-preheat-policy-update.1 @@ -0,0 +1,46 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-project-preheat-policy-update - Update a preheat policy + + +.SH SYNOPSIS +\fBharbor project preheat policy update [flags]\fP + + +.SH DESCRIPTION +Update an existing P2P preheat policy under a project + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for update + +.PP +\fB--id\fP[=false] + Use project id instead of name + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor project preheat policy update [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy update [PROJECT_ID] [POLICY_NAME] --id +.EE + + +.SH SEE ALSO +\fBharbor-project-preheat-policy(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-project-preheat-policy-view.1 b/doc/man-docs/man1/harbor-project-preheat-policy-view.1 new file mode 100644 index 000000000..a07929d52 --- /dev/null +++ b/doc/man-docs/man1/harbor-project-preheat-policy-view.1 @@ -0,0 +1,46 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-project-preheat-policy-view - View details of a preheat policy + + +.SH SYNOPSIS +\fBharbor project preheat policy view [flags]\fP + + +.SH DESCRIPTION +Get details of a specific P2P preheat policy under a project + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for view + +.PP +\fB--id\fP[=false] + Get preheat policy by project id + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor project preheat policy view [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy view [PROJECT_ID] [POLICY_NAME] --id +.EE + + +.SH SEE ALSO +\fBharbor-project-preheat-policy(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-project-preheat-policy.1 b/doc/man-docs/man1/harbor-project-preheat-policy.1 new file mode 100644 index 000000000..2924c2c1d --- /dev/null +++ b/doc/man-docs/man1/harbor-project-preheat-policy.1 @@ -0,0 +1,45 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-project-preheat-policy - Manage preheat policies + + +.SH SYNOPSIS +\fBharbor project preheat policy [flags]\fP + + +.SH DESCRIPTION +Manage P2P preheat policies under a project + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for policy + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor project preheat policy list [PROJECT_NAME] + harbor project preheat policy list [PROJECT_ID] --id + harbor project preheat policy create -f [CONFIG_FILE] + harbor project preheat policy view [PROJECT_NAME] [POLICY_NAME] + harbor project preheat policy start [PROJECT_NAME] [POLICY_NAME] +.EE + + +.SH SEE ALSO +\fBharbor-project-preheat(1)\fP, \fBharbor-project-preheat-policy-create(1)\fP, \fBharbor-project-preheat-policy-delete(1)\fP, \fBharbor-project-preheat-policy-list(1)\fP, \fBharbor-project-preheat-policy-start(1)\fP, \fBharbor-project-preheat-policy-update(1)\fP, \fBharbor-project-preheat-policy-view(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-project-preheat.1 b/doc/man-docs/man1/harbor-project-preheat.1 new file mode 100644 index 000000000..6824d1233 --- /dev/null +++ b/doc/man-docs/man1/harbor-project-preheat.1 @@ -0,0 +1,44 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-project-preheat - Manage project preheat resources + + +.SH SYNOPSIS +\fBharbor project preheat [flags]\fP + + +.SH DESCRIPTION +Manage project-scoped P2P preheat policies, executions, and tasks in Harbor + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for preheat + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor project preheat policy list [PROJECT_NAME] + harbor project preheat policy list [PROJECT_ID] --id + harbor project preheat policy create -f [CONFIG_FILE] + harbor project preheat policy start [PROJECT_NAME] [POLICY_NAME] +.EE + + +.SH SEE ALSO +\fBharbor-project(1)\fP, \fBharbor-project-preheat-policy(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-project.1 b/doc/man-docs/man1/harbor-project.1 index 0ad3d4256..42ef05b1f 100644 --- a/doc/man-docs/man1/harbor-project.1 +++ b/doc/man-docs/man1/harbor-project.1 @@ -38,4 +38,4 @@ Manage projects in Harbor .SH SEE ALSO -\fBharbor(1)\fP, \fBharbor-project-config(1)\fP, \fBharbor-project-create(1)\fP, \fBharbor-project-delete(1)\fP, \fBharbor-project-list(1)\fP, \fBharbor-project-logs(1)\fP, \fBharbor-project-member(1)\fP, \fBharbor-project-robot(1)\fP, \fBharbor-project-search(1)\fP, \fBharbor-project-view(1)\fP \ No newline at end of file +\fBharbor(1)\fP, \fBharbor-project-config(1)\fP, \fBharbor-project-create(1)\fP, \fBharbor-project-delete(1)\fP, \fBharbor-project-list(1)\fP, \fBharbor-project-logs(1)\fP, \fBharbor-project-member(1)\fP, \fBharbor-project-preheat(1)\fP, \fBharbor-project-robot(1)\fP, \fBharbor-project-search(1)\fP, \fBharbor-project-view(1)\fP \ No newline at end of file diff --git a/pkg/api/preheat_handler.go b/pkg/api/preheat_handler.go new file mode 100644 index 000000000..da52a72ce --- /dev/null +++ b/pkg/api/preheat_handler.go @@ -0,0 +1,154 @@ +// Copyright Project Harbor 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 api + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/preheat" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" +) + +func ListPreheatPolicies(projectName string, isID bool, opts ...ListFlags) (*preheat.ListPoliciesOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + var listFlags ListFlags + if len(opts) > 0 { + listFlags = opts[0] + } + + if isID { + project, err := GetProject(projectName, true) + if err != nil { + return nil, err + } + projectName = project.Payload.Name + } + + response, err := client.Preheat.ListPolicies(ctx, &preheat.ListPoliciesParams{ + ProjectName: projectName, + Page: &listFlags.Page, + PageSize: &listFlags.PageSize, + Q: &listFlags.Q, + Sort: &listFlags.Sort, + }) + if err != nil { + return nil, err + } + return response, nil +} + +func GetPreheatPolicy(projectName, policyName string) (*preheat.GetPolicyOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + response, err := client.Preheat.GetPolicy(ctx, &preheat.GetPolicyParams{ + ProjectName: projectName, + PreheatPolicyName: policyName, + }) + if err != nil { + return nil, err + } + return response, nil +} + +func DeletePreheatPolicy(projectName, policyName string) error { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + + _, err = client.Preheat.DeletePolicy(ctx, &preheat.DeletePolicyParams{ + ProjectName: projectName, + PreheatPolicyName: policyName, + }) + if err != nil { + return err + } + return nil +} + +func StartPreheatPolicy(projectName, policyName string) (string, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return "", err + } + + policy, err := GetPreheatPolicy(projectName, policyName) + if err != nil { + return "", err + } + + resp, err := client.Preheat.ManualPreheat(ctx, &preheat.ManualPreheatParams{ + ProjectName: projectName, + PreheatPolicyName: policyName, + Policy: policy.Payload, + }) + if err != nil { + return "", err + } + return resp.Location, nil +} + +func CreatePreheatPolicy(projectName string, policy *models.PreheatPolicy) (*preheat.CreatePolicyCreated, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + response, err := client.Preheat.CreatePolicy(ctx, &preheat.CreatePolicyParams{ + ProjectName: projectName, + Policy: policy, + }) + if err != nil { + return nil, err + } + return response, nil +} + +func UpdatePreheatPolicy(projectName, policyName string, policy *models.PreheatPolicy) (*preheat.UpdatePolicyOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + response, err := client.Preheat.UpdatePolicy(ctx, &preheat.UpdatePolicyParams{ + ProjectName: projectName, + PreheatPolicyName: policyName, + Policy: policy, + }) + if err != nil { + return nil, err + } + return response, nil +} + +func ListProvidersUnderProject(projectName string) ([]*models.ProviderUnderProject, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + response, err := client.Preheat.ListProvidersUnderProject(ctx, &preheat.ListProvidersUnderProjectParams{ + ProjectName: projectName, + }) + if err != nil { + return nil, err + } + return response.Payload, nil +} diff --git a/pkg/config/preheat/policies.go b/pkg/config/preheat/policies.go new file mode 100644 index 000000000..7592c77ee --- /dev/null +++ b/pkg/config/preheat/policies.go @@ -0,0 +1,253 @@ +// Copyright Project Harbor 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 config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + policycreate "github.com/goharbor/harbor-cli/pkg/views/preheat/policy/create" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +type PolicyConfig struct { + ProjectName string `yaml:"project_name" json:"project_name"` + Name string `yaml:"name" json:"name"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` + Filters []*PolicyFilter `yaml:"filters" json:"filters"` + ProviderName string `yaml:"provider_name" json:"provider_name"` + Trigger *PolicyTrigger `yaml:"trigger,omitempty" json:"trigger,omitempty"` +} + +type PolicyFilter struct { + Type string `yaml:"type" json:"type"` + Value string `yaml:"value" json:"value"` +} + +type PolicyTriggerSetting struct { + SchedulePreset string `yaml:"schedule_preset,omitempty" json:"schedule_preset,omitempty"` + Cron string `yaml:"cron,omitempty" json:"cron,omitempty"` +} + +type PolicyTrigger struct { + Type string `yaml:"type" json:"type"` + TriggerSetting *PolicyTriggerSetting `yaml:"trigger_setting,omitempty" json:"trigger_setting,omitempty"` +} + +func LoadConfigFromFile(filename string) (*policycreate.CreateView, error) { + var opts *policycreate.CreateView + var err error + + ext := filepath.Ext(filename) + if ext == "" { + return nil, fmt.Errorf("file must have an extension (.yaml, .yml, or .json)") + } + + fileType := ext[1:] + if fileType == "yml" { + fileType = "yaml" + } + + opts, err = LoadConfigFromYAMLorJSON(filename, fileType) + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %v", err) + } + return opts, nil +} + +func LoadConfigFromYAMLorJSON(filename string, fileType string) (*policycreate.CreateView, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read file: %v", err) + } + log.Debug("Preheat policy config file read successfully") + + var config PolicyConfig + switch fileType { + case "yaml", "yml": + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse YAML: %v", err) + } + log.Debugf("Parsed %s configuration successfully", fileType) + + case "json": + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %v", err) + } + log.Debugf("Parsed %s configuration successfully", fileType) + default: + return nil, fmt.Errorf("unsupported file type: %s, expected 'yaml' or 'json'", fileType) + } + + if err := validateConfig(&config); err != nil { + return nil, fmt.Errorf("configuration validation failed: %v", err) + } + log.Debug("Preheat policy configuration validated successfully") + + triggerType := "manual" + cronString := "" + if config.Trigger != nil { + triggerType = normalizeTriggerMode(config.Trigger.Type) + if triggerType == "scheduled" { + var err error + cronString, err = resolveTriggerCron(config.Trigger) + if err != nil { + return nil, err + } + } + } + + opts := &policycreate.CreateView{ + ProjectName: config.ProjectName, + Name: config.Name, + Description: config.Description, + ProviderName: config.ProviderName, + TriggerType: triggerType, + CronString: cronString, + Enabled: config.Enabled, + } + + for _, filter := range config.Filters { + switch filter.Type { + case "repository": + opts.RepositoryFilter = filter.Value + case "tag": + opts.TagFilter = filter.Value + case "label": + opts.LabelFilter = filter.Value + } + } + + return opts, nil +} + +func validateConfig(config *PolicyConfig) error { + if config.ProjectName == "" { + return fmt.Errorf("project_name is required") + } + + if config.Name == "" { + return fmt.Errorf("name is required") + } + + if config.ProviderName == "" { + return fmt.Errorf("provider_name is required") + } + + if err := validatePolicyFilters(config.Filters); err != nil { + return err + } + + if config.Trigger == nil { + return nil + } + + if strings.TrimSpace(config.Trigger.Type) == "" { + return fmt.Errorf("trigger.type is required") + } + + triggerType := strings.ToLower(config.Trigger.Type) + switch triggerType { + case "manual", "scheduled", "event_based": + default: + return fmt.Errorf("trigger.type must be one of [manual, scheduled, event_based], got: %s", config.Trigger.Type) + } + + if triggerType != "scheduled" { + setting := config.Trigger.TriggerSetting + if setting != nil && (strings.TrimSpace(setting.SchedulePreset) != "" || strings.TrimSpace(setting.Cron) != "") { + return fmt.Errorf("trigger.trigger_setting is only supported for scheduled trigger") + } + return nil + } + + _, err := resolveTriggerCron(config.Trigger) + return err +} + +func resolveTriggerCron(trigger *PolicyTrigger) (string, error) { + if trigger == nil || trigger.TriggerSetting == nil { + return "", nil + } + + setting := trigger.TriggerSetting + preset := strings.ToLower(strings.TrimSpace(setting.SchedulePreset)) + switch preset { + case "": + return strings.TrimSpace(setting.Cron), nil + case "none": + return "", nil + case "hourly", "daily", "weekly", "custom": + cron := policycreate.ResolveSchedulePreset(preset, setting.Cron) + if preset == "custom" && cron == "" { + return "", fmt.Errorf("trigger.trigger_setting.cron is required for custom schedule") + } + return cron, nil + default: + return "", fmt.Errorf("trigger.trigger_setting.schedule_preset must be one of [none, hourly, daily, weekly, custom], got: %s", setting.SchedulePreset) + } +} + +func validatePolicyFilters(filters []*PolicyFilter) error { + if len(filters) < 2 || len(filters) > 3 { + return fmt.Errorf("filters must include repository and tag filters, with label optional") + } + + hasRepositoryFilter := false + hasTagFilter := false + for i, filter := range filters { + if filter == nil { + return fmt.Errorf("filters[%d] is required", i) + } + + switch filter.Type { + case "repository": + hasRepositoryFilter = true + case "tag": + hasTagFilter = true + case "label": + default: + return fmt.Errorf("filters[%d].type must be one of [repository, tag, label], got: %s", i, filter.Type) + } + + if strings.TrimSpace(filter.Value) == "" { + return fmt.Errorf("filters[%d].value is required", i) + } + } + + if !hasRepositoryFilter { + return fmt.Errorf("filters must include a repository filter") + } + if !hasTagFilter { + return fmt.Errorf("filters must include a tag filter") + } + + return nil +} + +func normalizeTriggerMode(mode string) string { + switch strings.ToLower(mode) { + case "scheduled": + return "scheduled" + case "event_based": + return "event_based" + default: + return "manual" + } +} diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index f21932e5f..68aede286 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -37,6 +37,8 @@ import ( rpolicies "github.com/goharbor/harbor-cli/pkg/views/replication/policies/select" rtasks "github.com/goharbor/harbor-cli/pkg/views/replication/task/select" + phpolicies "github.com/goharbor/harbor-cli/pkg/views/preheat/policy/select" + repoView "github.com/goharbor/harbor-cli/pkg/views/repository/select" retview "github.com/goharbor/harbor-cli/pkg/views/retention/select" robotView "github.com/goharbor/harbor-cli/pkg/views/robot/select" @@ -467,3 +469,39 @@ func GetRetentionTagRule(retentionID string) int64 { }() return <-retentionIndex } + +func GetPreheatPolicyNameFromUser(projectName string) (string, error) { + type result struct { + name string + err error + } + resultChan := make(chan result) + + go func() { + response, err := api.ListPreheatPolicies(projectName, false) + if err != nil { + resultChan <- result{"", err} + return + } + + if len(response.Payload) == 0 { + resultChan <- result{"", errors.New("no preheat policies found")} + return + } + + name, err := phpolicies.PreheatPolicyList(response.Payload) + if err != nil { + if err == phpolicies.ErrUserAborted { + resultChan <- result{"", errors.New("user aborted policy selection")} + } else { + resultChan <- result{"", fmt.Errorf("error during policy selection: %w", err)} + } + return + } + + resultChan <- result{name, nil} + }() + + res := <-resultChan + return res.name, res.err +} diff --git a/pkg/views/preheat/policy/create/view.go b/pkg/views/preheat/policy/create/view.go new file mode 100644 index 000000000..cd8f1c69a --- /dev/null +++ b/pkg/views/preheat/policy/create/view.go @@ -0,0 +1,224 @@ +// Copyright Project Harbor 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 create + +import ( + "errors" + "fmt" + "strings" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + log "github.com/sirupsen/logrus" +) + +type CreateView struct { + ProjectName string `json:"project_name,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Enabled bool `json:"enabled,omitempty"` + + // Provider related fields + ProviderName string `json:"provider_name,omitempty"` + + // Filter related fields + RepositoryFilter string `json:"repository_filter,omitempty"` + TagFilter string `json:"tag_filter,omitempty"` + LabelFilter string `json:"label_filter,omitempty"` + + // Trigger related fields + TriggerType string `json:"trigger_type,omitempty"` + CronString string `json:"cron_string,omitempty"` +} + +func CreatePreheatPolicyView(createView *CreateView, providers []*models.ProviderUnderProject) { + if createView.TriggerType == "" { + createView.TriggerType = "manual" + } + + theme := huh.ThemeCharm() + + if len(providers) == 0 { + log.Fatal("No P2P provider instances available for this project. Please create a provider instance first.") + } + + providerOptions := make([]huh.Option[string], 0, len(providers)) + for _, p := range providers { + if !p.Enabled { + continue + } + label := fmt.Sprintf("%s (ID: %d)", p.Provider, p.ID) + providerOptions = append(providerOptions, huh.NewOption(label, p.Provider)) + } + + if len(providerOptions) == 0 { + log.Fatal("No enabled P2P provider instances available for this project.") + } + + basicGroup := huh.NewGroup( + huh.NewSelect[string](). + Title("Provider"). + Options(providerOptions...). + Value(&createView.ProviderName), + huh.NewInput(). + Title("Policy Name"). + Value(&createView.Name). + Validate(func(str string) error { + if strings.TrimSpace(str) == "" { + return errors.New("policy name cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("Description"). + Value(&createView.Description), + huh.NewConfirm(). + Title("Enabled"). + Value(&createView.Enabled). + WithButtonAlignment(lipgloss.Left), + ) + + basicForm := huh.NewForm(basicGroup).WithTheme(theme) + if err := basicForm.Run(); err != nil { + log.Fatal(err) + } + + filterGroup := huh.NewGroup( + huh.NewInput(). + Title("Repositories"). + Description("Enter multiple comma separated repos, repo*, or **"). + Value(&createView.RepositoryFilter). + Validate(func(str string) error { + if strings.TrimSpace(str) == "" { + return errors.New("repository filter cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("Tags"). + Description("Enter multiple comma separated tags, tag*, or **"). + Value(&createView.TagFilter). + Validate(func(str string) error { + if strings.TrimSpace(str) == "" { + return errors.New("tag filter cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("Labels"). + Description("(optional)"). + Value(&createView.LabelFilter), + ).Title("Filters") + + triggerGroup := huh.NewGroup( + huh.NewSelect[string](). + Title("Trigger Type"). + Options( + huh.NewOption("Manual", "manual"), + huh.NewOption("Scheduled", "scheduled"), + huh.NewOption("Event Based", "event_based"), + ). + Value(&createView.TriggerType), + ) + + restForm := huh.NewForm(filterGroup, triggerGroup).WithTheme(theme) + if err := restForm.Run(); err != nil { + log.Fatal(err) + } + + if createView.TriggerType == "scheduled" { + schedulePreset := schedulePresetForCron(createView.CronString) + presetForm := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Schedule"). + Description("Choose a schedule frequency for preheating"). + Options( + huh.NewOption("None", "none"), + huh.NewOption("Hourly", "hourly"), + huh.NewOption("Daily", "daily"), + huh.NewOption("Weekly", "weekly"), + huh.NewOption("Custom", "custom"), + ). + Value(&schedulePreset), + ), + ).WithTheme(theme) + + if err := presetForm.Run(); err != nil { + log.Fatal(err) + } + + if schedulePreset == "custom" { + cronForm := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Cron String"). + Description("Schedule using 6-field cron format: seconds minutes hours day-month month day-week"). + Placeholder("0 0 0 * * *"). + Value(&createView.CronString). + Validate(func(s string) error { + if strings.TrimSpace(s) == "" { + return errors.New("cron string cannot be empty for custom schedule") + } + + fields := strings.Fields(s) + if len(fields) != 6 { + return fmt.Errorf("cron must have exactly 6 fields (found %d): seconds minutes hours day-month month day-week", len(fields)) + } + + return nil + }), + ), + ).WithTheme(theme) + + if err := cronForm.Run(); err != nil { + log.Fatal(err) + } + createView.CronString = ResolveSchedulePreset(schedulePreset, createView.CronString) + } else { + createView.CronString = ResolveSchedulePreset(schedulePreset, "") + } + } +} + +func schedulePresetForCron(cron string) string { + switch strings.TrimSpace(cron) { + case "": + return "none" + case "0 0 * * * *": + return "hourly" + case "0 0 0 * * *": + return "daily" + case "0 0 0 * * 0": + return "weekly" + default: + return "custom" + } +} + +func ResolveSchedulePreset(preset, cron string) string { + switch preset { + case "hourly": + return "0 0 * * * *" + case "daily": + return "0 0 0 * * *" + case "weekly": + return "0 0 0 * * 0" + case "custom": + return strings.TrimSpace(cron) + default: + return "" + } +} diff --git a/pkg/views/preheat/policy/list/view.go b/pkg/views/preheat/policy/list/view.go new file mode 100644 index 000000000..a98910aa7 --- /dev/null +++ b/pkg/views/preheat/policy/list/view.go @@ -0,0 +1,83 @@ +// Copyright Project Harbor 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 list + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "Name", Width: tablelist.WidthM}, + {Title: "Enabled", Width: tablelist.WidthS}, + {Title: "Provider", Width: tablelist.WidthXL}, + {Title: "Filters", Width: tablelist.Width3XL}, + {Title: "Trigger", Width: tablelist.WidthM}, + {Title: "Creation Time", Width: tablelist.WidthL}, + {Title: "Description", Width: tablelist.WidthM}, +} + +func ListPolicies(policies []*models.PreheatPolicy) { + var rows []table.Row + for _, policy := range policies { + enabled := "No" + if policy.Enabled { + enabled = "Yes" + } + + createdTime, _ := utils.FormatCreatedTime(policy.CreationTime.String()) + + var filters []struct { + Type string `json:"type"` + Value string `json:"value"` + } + var filterParts []string + if err := json.Unmarshal([]byte(policy.Filters), &filters); err == nil { + for _, f := range filters { + filterParts = append(filterParts, fmt.Sprintf("%s: %s", f.Type, f.Value)) + } + } + + var trigger struct { + Type string `json:"type"` + } + if err := json.Unmarshal([]byte(policy.Trigger), &trigger); err != nil { + trigger.Type = policy.Trigger + } + + rows = append(rows, table.Row{ + policy.Name, + enabled, + policy.ProviderName, + strings.Join(filterParts, " "), + trigger.Type, + createdTime, + policy.Description, + }) + } + + m := tablelist.NewModel(columns, rows, len(rows)) + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/pkg/views/preheat/policy/select/view.go b/pkg/views/preheat/policy/select/view.go new file mode 100644 index 000000000..21d584006 --- /dev/null +++ b/pkg/views/preheat/policy/select/view.go @@ -0,0 +1,54 @@ +// Copyright Project Harbor 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 policy + +import ( + "errors" + "fmt" + "os" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/views/base/selection" +) + +var ErrUserAborted = errors.New("user aborted selection") + +func PreheatPolicyList(policies []*models.PreheatPolicy) (string, error) { + items := make([]list.Item, len(policies)) + for i, p := range policies { + items[i] = selection.Item(p.Name) + } + + m := selection.NewModel(items, "Preheat Policy") + + p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } + + if model, ok := p.(selection.Model); ok { + if model.Aborted { + return "", ErrUserAborted + } + if model.Choice == "" { + return "", errors.New("no policy selected") + } + return model.Choice, nil + } + + return "", errors.New("unexpected program result") +} diff --git a/pkg/views/preheat/policy/view/view.go b/pkg/views/preheat/policy/view/view.go new file mode 100644 index 000000000..e13704b21 --- /dev/null +++ b/pkg/views/preheat/policy/view/view.go @@ -0,0 +1,85 @@ +// Copyright Project Harbor 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 view + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "Name", Width: tablelist.WidthM}, + {Title: "Enabled", Width: tablelist.WidthS}, + {Title: "Provider", Width: tablelist.WidthXL}, + {Title: "Filters", Width: tablelist.Width3XL}, + {Title: "Trigger", Width: tablelist.WidthM}, + {Title: "Creation Time", Width: tablelist.WidthL}, + {Title: "Updated", Width: tablelist.WidthL}, + {Title: "Description", Width: tablelist.WidthM}, +} + +func ViewPolicy(policy *models.PreheatPolicy) { + var rows []table.Row + + enabled := "No" + if policy.Enabled { + enabled = "Yes" + } + + createdTime, _ := utils.FormatCreatedTime(policy.CreationTime.String()) + updatedTime, _ := utils.FormatCreatedTime(policy.UpdateTime.String()) + + var filters []struct { + Type string `json:"type"` + Value string `json:"value"` + } + var filterParts []string + if err := json.Unmarshal([]byte(policy.Filters), &filters); err == nil { + for _, f := range filters { + filterParts = append(filterParts, fmt.Sprintf("%s: %s", f.Type, f.Value)) + } + } + + var trigger struct { + Type string `json:"type"` + } + if err := json.Unmarshal([]byte(policy.Trigger), &trigger); err != nil { + trigger.Type = policy.Trigger + } + + rows = append(rows, table.Row{ + policy.Name, + enabled, + policy.ProviderName, + strings.Join(filterParts, " "), + trigger.Type, + createdTime, + updatedTime, + policy.Description, + }) + + m := tablelist.NewModel(columns, rows, len(rows)) + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +}