diff --git a/plugins/ibmcloud/api_key.go b/plugins/ibmcloud/api_key.go new file mode 100644 index 00000000..944826ca --- /dev/null +++ b/plugins/ibmcloud/api_key.go @@ -0,0 +1,45 @@ +package ibmcloud + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/importer" + "github.com/1Password/shell-plugins/sdk/provision" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func APIKey() schema.CredentialType { + return schema.CredentialType{ + Name: credname.APIKey, + DocsURL: sdk.URL("https://cloud.ibm.com/docs/account?topic=account-manapikey"), + ManagementURL: sdk.URL("https://cloud.ibm.com/iam/overview"), + Fields: []schema.CredentialField{ + { + Name: fieldname.APIKey, + MarkdownDescription: "API Key used to authenticate to IBM Cloud.", + Secret: true, + Composition: &schema.ValueComposition{ + Length: 44, + Charset: schema.Charset{ + Uppercase: true, + Lowercase: true, + Digits: true, + }, + }, + }, + { + Name: sdk.FieldName("resource group"), + MarkdownDescription: "Resource group to target when logging in to IBM Cloud.", + Optional: true, + }, + }, + DefaultProvisioner: provision.EnvVars(defaultEnvVarMapping), + Importer: importer.TryAll( + importer.TryEnvVarPair(defaultEnvVarMapping), + )} +} + +var defaultEnvVarMapping = map[string]sdk.FieldName{ + "IBMCLOUD_API_KEY": fieldname.APIKey, +} diff --git a/plugins/ibmcloud/api_key_test.go b/plugins/ibmcloud/api_key_test.go new file mode 100644 index 00000000..f8577cc8 --- /dev/null +++ b/plugins/ibmcloud/api_key_test.go @@ -0,0 +1,45 @@ +package ibmcloud + +import ( + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func TestAPIKeyProvisioner(t *testing.T) { + plugintest.TestProvisioner(t, APIKey().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "default": { + ItemFields: map[sdk.FieldName]string{ + fieldname.APIKey: "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "IBMCLOUD_API_KEY": "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + }, + }, + }, + }) +} + +func TestAPIKeyImporter(t *testing.T) { + plugintest.TestImporter(t, APIKey().Importer, map[string]plugintest.ImportCase{ + "environment": { + Environment: map[string]string{ + "IBMCLOUD_API_KEY": "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.APIKey: "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + }, + }, + }, + }, + "config file": { + Files: map[string]string{}, + ExpectedCandidates: []sdk.ImportCandidate{}, + }, + }) +} diff --git a/plugins/ibmcloud/cli_provisioner.go b/plugins/ibmcloud/cli_provisioner.go new file mode 100644 index 00000000..c820c777 --- /dev/null +++ b/plugins/ibmcloud/cli_provisioner.go @@ -0,0 +1,70 @@ +package ibmcloud + +import ( + "context" + "strings" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +// CLIProvisioner is responsible for provisioning the IBM Cloud CLI with the appropriate credentials +type CLIProvisioner struct { +} + +func (p CLIProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) { + // First, set the API key as an environment variable + out.AddEnvVar("IBMCLOUD_API_KEY", in.ItemFields[fieldname.APIKey]) + + // Check if we need to add a resource group flag to the login command + targetGroup := "" + found := false + // Try exact match first + if tg, ok := in.ItemFields[sdk.FieldName("resource group")]; ok && tg != "" { + targetGroup = tg + found = true + } + + // Try with trimmed whitespace + if !found { + for fieldName, fieldValue := range in.ItemFields { + fieldNameStr := string(fieldName) + trimmedName := strings.TrimSpace(fieldNameStr) + if trimmedName == "resource group" && fieldValue != "" { + targetGroup = fieldValue + found = true + break + } + } + } + + if found && targetGroup != "" { + // Only modify the command line if it's an ibmcloud login command + if len(out.CommandLine) > 1 && out.CommandLine[1] == "login" { + // Check if the command already has a -g or --target-group flag + hasTargetGroupFlag := false + for _, arg := range out.CommandLine { + if arg == "-g" || arg == "--target-group" || strings.HasPrefix(arg, "-g=") || strings.HasPrefix(arg, "--target-group=") { + hasTargetGroupFlag = true + break + } + } + + // If no resource group flag is present, add it + if !hasTargetGroupFlag { + newCommandLine := append([]string{}, out.CommandLine[0], out.CommandLine[1]) + newCommandLine = append(newCommandLine, "-g", targetGroup) + newCommandLine = append(newCommandLine, out.CommandLine[2:]...) + out.CommandLine = newCommandLine + } + } + } +} + +func (p CLIProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) { + // Nothing to do here: environment variables get wiped automatically when the process exits. +} + +func (p CLIProvisioner) Description() string { + return "Provision environment variables with IBM Cloud API Key and add resource group flag to login command if specified" +} diff --git a/plugins/ibmcloud/cli_provisioner_test.go b/plugins/ibmcloud/cli_provisioner_test.go new file mode 100644 index 00000000..f6df8e31 --- /dev/null +++ b/plugins/ibmcloud/cli_provisioner_test.go @@ -0,0 +1,69 @@ +package ibmcloud + +import ( + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func TestCLIProvisioner(t *testing.T) { + provisioner := CLIProvisioner{} + + testCases := map[string]plugintest.ProvisionCase{ + "with api key only": { + ItemFields: map[sdk.FieldName]string{ + fieldname.APIKey: "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + }, + CommandLine: []string{"ibmcloud", "login"}, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "IBMCLOUD_API_KEY": "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + }, + CommandLine: []string{"ibmcloud", "login"}, + }, + }, + "with api key and resource group": { + ItemFields: map[sdk.FieldName]string{ + fieldname.APIKey: "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + sdk.FieldName("resource group"): "my-resource-group", + }, + CommandLine: []string{"ibmcloud", "login"}, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "IBMCLOUD_API_KEY": "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + }, + CommandLine: []string{"ibmcloud", "login", "-g", "my-resource-group"}, + }, + }, + "with resource group but not login command": { + ItemFields: map[sdk.FieldName]string{ + fieldname.APIKey: "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + sdk.FieldName("resource group"): "my-resource-group", + }, + CommandLine: []string{"target"}, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "IBMCLOUD_API_KEY": "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + }, + CommandLine: []string{"target"}, + }, + }, + "with resource group and existing group flag": { + ItemFields: map[sdk.FieldName]string{ + fieldname.APIKey: "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + sdk.FieldName("resource group"): "my-resource-group", + }, + CommandLine: []string{"login", "-g", "existing-group"}, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "IBMCLOUD_API_KEY": "GeYS3RmGXo7cQhY8UboUSLmWarFF1HGqv4fVKEXAMPLE", + }, + CommandLine: []string{"login", "-g", "existing-group"}, + }, + }, + } + + plugintest.TestProvisioner(t, provisioner, testCases) +} diff --git a/plugins/ibmcloud/ibmcloud.go b/plugins/ibmcloud/ibmcloud.go new file mode 100644 index 00000000..27cc042a --- /dev/null +++ b/plugins/ibmcloud/ibmcloud.go @@ -0,0 +1,26 @@ +package ibmcloud + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" +) + +func IBMCloudCLI() schema.Executable { + return schema.Executable{ + Name: "IBM Cloud CLI", + Runs: []string{"ibmcloud"}, + DocsURL: sdk.URL("https://ibmcloud.com/docs/cli"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.APIKey, + Provisioner: CLIProvisioner{}, + }, + }, + } +} diff --git a/plugins/ibmcloud/plugin.go b/plugins/ibmcloud/plugin.go new file mode 100644 index 00000000..8f5b074e --- /dev/null +++ b/plugins/ibmcloud/plugin.go @@ -0,0 +1,22 @@ +package ibmcloud + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "ibmcloud", + Platform: schema.PlatformInfo{ + Name: "IBM Cloud", + Homepage: sdk.URL("https://cloud.ibm.com/"), + }, + Credentials: []schema.CredentialType{ + APIKey(), + }, + Executables: []schema.Executable{ + IBMCloudCLI(), + }, + } +}