diff --git a/plugins/oci/api_key.go b/plugins/oci/api_key.go new file mode 100644 index 00000000..a75b8fa9 --- /dev/null +++ b/plugins/oci/api_key.go @@ -0,0 +1,109 @@ +package oci + +import ( + "context" + "os" + "path/filepath" + "strings" + + "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://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm"), + ManagementURL: sdk.URL("https://cloud.oracle.com"), + Fields: []schema.CredentialField{ + { + Name: fieldname.User, + MarkdownDescription: "OCID of the user used to authenticate to OCI.", + }, + { + Name: tenantIDField, + MarkdownDescription: "OCID of the tenant used to authenticate to OCI.", + }, + { + Name: fingerprintField, + MarkdownDescription: "Fingerprint of the API key used to authenticate to OCI.", + }, + { + Name: fieldname.Region, + MarkdownDescription: "OCI region to use.", + Optional: true, + }, + { + Name: fieldname.PrivateKey, + MarkdownDescription: "Private API key used to authenticate to OCI.", + Secret: true, + }, + }, + DefaultProvisioner: provision.EnvVars(defaultEnvVarMapping), + Importer: importer.TryAll( + importer.TryEnvVarPair(defaultEnvVarMapping), + TryOCIConfigFile(), + )} +} + +var ( + tenantIDField = sdk.FieldName("Tenant ID") + fingerprintField = sdk.FieldName("Fingerprint") +) + +var defaultEnvVarMapping = map[string]sdk.FieldName{ + "OCI_CLI_USER": fieldname.User, + "OCI_CLI_TENANCY": tenantIDField, + "OCI_CLI_FINGERPRINT": fingerprintField, + "OCI_CLI_REGION": fieldname.Region, + "OCI_CLI_KEY_CONTENT": fieldname.PrivateKey, +} + +func TryOCIConfigFile() sdk.Importer { + return importer.TryFile("~/.oci/config", func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) { + config, err := contents.ToINI() + if err != nil { + out.AddError(err) + return + } + + for _, section := range config.Sections() { + fields := map[sdk.FieldName]string{ + fieldname.User: section.Key("user").String(), + tenantIDField: section.Key("tenancy").String(), + fingerprintField: section.Key("fingerprint").String(), + } + + if region := section.Key("region").String(); region != "" { + fields[fieldname.Region] = region + } + + keyFile := section.Key("key_file").String() + if fields[fieldname.User] == "" || fields[tenantIDField] == "" || fields[fingerprintField] == "" || keyFile == "" { + continue + } + + if after, ok := strings.CutPrefix(keyFile, "~/"); ok { + keyFile = in.FromHomeDir(after) + } else if filepath.IsAbs(keyFile) { + keyFile = in.FromRootDir(keyFile) + } + + privateKey, err := os.ReadFile(keyFile) + if err != nil { + out.AddError(err) + continue + } + fields[fieldname.PrivateKey] = string(privateKey) + + out.AddCandidate(sdk.ImportCandidate{ + NameHint: section.Name(), + Fields: fields, + }) + } + }) +} diff --git a/plugins/oci/api_key_test.go b/plugins/oci/api_key_test.go new file mode 100644 index 00000000..3d68642f --- /dev/null +++ b/plugins/oci/api_key_test.go @@ -0,0 +1,85 @@ +package oci + +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.User: "ocid1.user.oc1..example", + tenantIDField: "ocid1.tenancy.oc1..example", + fingerprintField: "20:3b:97:13:55:1c:example", + fieldname.Region: "us-ashburn-1", + fieldname.PrivateKey: "-----BEGIN PRIVATE KEY-----\nEXAMPLE\n-----END PRIVATE KEY-----", + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "OCI_CLI_USER": "ocid1.user.oc1..example", + "OCI_CLI_TENANCY": "ocid1.tenancy.oc1..example", + "OCI_CLI_FINGERPRINT": "20:3b:97:13:55:1c:example", + "OCI_CLI_REGION": "us-ashburn-1", + "OCI_CLI_KEY_CONTENT": "-----BEGIN PRIVATE KEY-----\nEXAMPLE\n-----END PRIVATE KEY-----", + }, + }, + }, + }) +} + +func TestAPIKeyImporter(t *testing.T) { + plugintest.TestImporter(t, APIKey().Importer, map[string]plugintest.ImportCase{ + "environment": { + Environment: map[string]string{ + "OCI_CLI_USER": "ocid1.user.oc1..example", + "OCI_CLI_TENANCY": "ocid1.tenancy.oc1..example", + "OCI_CLI_FINGERPRINT": "20:3b:97:13:55:1c:example", + "OCI_CLI_REGION": "us-ashburn-1", + "OCI_CLI_KEY_CONTENT": "-----BEGIN PRIVATE KEY-----\nEXAMPLE\n-----END PRIVATE KEY-----", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.User: "ocid1.user.oc1..example", + tenantIDField: "ocid1.tenancy.oc1..example", + fingerprintField: "20:3b:97:13:55:1c:example", + fieldname.Region: "us-ashburn-1", + fieldname.PrivateKey: "-----BEGIN PRIVATE KEY-----\nEXAMPLE\n-----END PRIVATE KEY-----", + }, + }, + }, + }, + "config file": { + Files: map[string]string{ + "~/.oci/config": plugintest.LoadFixture(t, "config"), + "~/.oci/default.pem": "-----BEGIN PRIVATE KEY-----\nDEFAULT EXAMPLE\n-----END PRIVATE KEY-----", + "~/.oci/team.pem": "-----BEGIN PRIVATE KEY-----\nTEAM EXAMPLE\n-----END PRIVATE KEY-----", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + NameHint: "DEFAULT", + Fields: map[sdk.FieldName]string{ + fieldname.User: "ocid1.user.oc1..default-example", + tenantIDField: "ocid1.tenancy.oc1..default-example", + fingerprintField: "20:3b:97:13:55:1c:default-example", + fieldname.Region: "us-ashburn-1", + fieldname.PrivateKey: "-----BEGIN PRIVATE KEY-----\nDEFAULT EXAMPLE\n-----END PRIVATE KEY-----", + }, + }, + { + NameHint: "TEAM", + Fields: map[sdk.FieldName]string{ + fieldname.User: "ocid1.user.oc1..team-example", + tenantIDField: "ocid1.tenancy.oc1..team-example", + fingerprintField: "20:3b:97:13:55:1c:team-example", + fieldname.PrivateKey: "-----BEGIN PRIVATE KEY-----\nTEAM EXAMPLE\n-----END PRIVATE KEY-----", + }, + }, + }, + }, + }) +} diff --git a/plugins/oci/oci.go b/plugins/oci/oci.go new file mode 100644 index 00000000..371df208 --- /dev/null +++ b/plugins/oci/oci.go @@ -0,0 +1,25 @@ +package oci + +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 OCICLI() schema.Executable { + return schema.Executable{ + Name: "oci", + Runs: []string{"oci"}, + DocsURL: sdk.URL("https://docs.oracle.com/en-us/iaas/tools/oci-cli/latest/oci_cli_docs/"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.APIKey, + }, + }, + } +} diff --git a/plugins/oci/oci_test.go b/plugins/oci/oci_test.go new file mode 100644 index 00000000..0a34874d --- /dev/null +++ b/plugins/oci/oci_test.go @@ -0,0 +1,42 @@ +package oci + +import ( + "testing" + + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/credname" +) + +func TestOCICLI(t *testing.T) { + executable := OCICLI() + + if executable.Name != "oci" { + t.Errorf("Name = %q, want %q", executable.Name, "oci") + } + if len(executable.Runs) != 1 || executable.Runs[0] != "oci" { + t.Errorf("Runs = %v, want [oci]", executable.Runs) + } + if len(executable.Uses) != 1 || executable.Uses[0].Name != credname.APIKey { + t.Errorf("Uses = %v, want credential %q", executable.Uses, credname.APIKey) + } +} + +func TestOCICLINeedsAuth(t *testing.T) { + plugintest.TestNeedsAuth(t, OCICLI().NeedsAuth, map[string]plugintest.NeedsAuthCase{ + "without arguments": { + ExpectedNeedsAuth: false, + }, + "help": { + Args: []string{"--help"}, + ExpectedNeedsAuth: false, + }, + "version": { + Args: []string{"--version"}, + ExpectedNeedsAuth: false, + }, + "command": { + Args: []string{"os", "ns", "get"}, + ExpectedNeedsAuth: true, + }, + }) +} diff --git a/plugins/oci/plugin.go b/plugins/oci/plugin.go new file mode 100644 index 00000000..6ea42c8d --- /dev/null +++ b/plugins/oci/plugin.go @@ -0,0 +1,22 @@ +package oci + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "oci", + Platform: schema.PlatformInfo{ + Name: "OCI", + Homepage: sdk.URL("https://www.oracle.com/cloud/"), + }, + Credentials: []schema.CredentialType{ + APIKey(), + }, + Executables: []schema.Executable{ + OCICLI(), + }, + } +} diff --git a/plugins/oci/test-fixtures/config b/plugins/oci/test-fixtures/config new file mode 100644 index 00000000..e3eba054 --- /dev/null +++ b/plugins/oci/test-fixtures/config @@ -0,0 +1,12 @@ +[DEFAULT] +user=ocid1.user.oc1..default-example +tenancy=ocid1.tenancy.oc1..default-example +fingerprint=20:3b:97:13:55:1c:default-example +region=us-ashburn-1 +key_file=~/.oci/default.pem + +[TEAM] +user=ocid1.user.oc1..team-example +tenancy=ocid1.tenancy.oc1..team-example +fingerprint=20:3b:97:13:55:1c:team-example +key_file=~/.oci/team.pem