diff --git a/backend/api/migrations/0064_alter_providercredentials_provider.py b/backend/api/migrations/0064_alter_providercredentials_provider.py new file mode 100644 index 000000000..42ef88a65 --- /dev/null +++ b/backend/api/migrations/0064_alter_providercredentials_provider.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2024-05-13 09:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0063_lockbox'), + ] + + operations = [ + migrations.AlterField( + model_name='providercredentials', + name='provider', + field=models.CharField(choices=[('cloudflare', 'Cloudflare'), ('aws', 'AWS'), ('github', 'GitHub'), ('hashicorp_vault', 'Hashicorp Vault'), ('hashicorp_domad', 'Hashicorp Nomad')], max_length=50), + ), + ] diff --git a/backend/api/migrations/0065_alter_providercredentials_provider.py b/backend/api/migrations/0065_alter_providercredentials_provider.py new file mode 100644 index 000000000..68c6f4e50 --- /dev/null +++ b/backend/api/migrations/0065_alter_providercredentials_provider.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2024-05-13 09:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0064_alter_providercredentials_provider'), + ] + + operations = [ + migrations.AlterField( + model_name='providercredentials', + name='provider', + field=models.CharField(choices=[('cloudflare', 'Cloudflare'), ('aws', 'AWS'), ('github', 'GitHub'), ('hashicorp_vault', 'Hashicorp Vault'), ('hashicorp_nomad', 'Hashicorp Nomad')], max_length=50), + ), + ] diff --git a/backend/api/migrations/0066_alter_environmentsync_service.py b/backend/api/migrations/0066_alter_environmentsync_service.py new file mode 100644 index 000000000..fe9c2dc12 --- /dev/null +++ b/backend/api/migrations/0066_alter_environmentsync_service.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2024-05-13 12:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0065_alter_providercredentials_provider'), + ] + + operations = [ + migrations.AlterField( + model_name='environmentsync', + name='service', + field=models.CharField(choices=[('cloudflare_pages', 'Cloudflare Pages'), ('aws_secrets_manager', 'AWS Secrets Manager'), ('github_actions', 'GitHub Actions'), ('hashicorp_vault', 'Hashicorp Vault'), ('hashicorp_nomad', 'Hashicorp Nomad')], max_length=50), + ), + ] diff --git a/backend/api/services.py b/backend/api/services.py index 0f5acfefe..723e0c667 100644 --- a/backend/api/services.py +++ b/backend/api/services.py @@ -35,6 +35,17 @@ class Providers: "auth_scheme": "token", } + HASHICORP_NOMAD = { + "id": "hashicorp_nomad", + "name": "Hashicorp Nomad", + "expected_credentials": [ + "nomad_addr", + "nomad_token_secret", + ], + "optional_credentials": [], + "auth_scheme": "token", + } + @classmethod def get_provider_choices(cls): return [ @@ -86,6 +97,13 @@ class ServiceConfig: "resource_type": "path", } + HASHICORP_NOMAD = { + "id": "hashicorp_nomad", + "name": "Hashicorp Nomad", + "provider": Providers.HASHICORP_NOMAD, + "resource_type": "path", + } + @classmethod def get_service_choices(cls): return [ diff --git a/backend/api/tasks.py b/backend/api/tasks.py index 0eb275e1e..6d8d54f3e 100644 --- a/backend/api/tasks.py +++ b/backend/api/tasks.py @@ -7,6 +7,7 @@ sync_github_secrets, ) from api.utils.syncing.vault.main import sync_vault_secrets +from api.utils.syncing.nomad.main import sync_nomad_secrets from .utils.syncing.cloudflare.pages import ( get_cf_pages_credentials, sync_cloudflare_secrets, @@ -63,6 +64,15 @@ def trigger_sync_tasks(env_sync): EnvironmentSyncEvent.objects.create(id=job_id, env_sync=env_sync) + elif env_sync.service == ServiceConfig.HASHICORP_NOMAD["id"]: + env_sync.status = EnvironmentSync.IN_PROGRESS + env_sync.save() + + job = perform_nomad_sync.delay(env_sync) + job_id = job.get_id() + + EnvironmentSyncEvent.objects.create(id=job_id, env_sync=env_sync) + # try and cancel running or queued jobs for this sync def cancel_sync_tasks(env_sync): @@ -483,3 +493,82 @@ def perform_vault_sync(environment_sync): environment_sync.last_sync = timezone.now() environment_sync.status = EnvironmentSync.FAILED environment_sync.save() + + +@job("default", timeout=3600) +def perform_nomad_sync(environment_sync): + try: + EnvironmentSync = apps.get_model("api", "EnvironmentSync") + EnvironmentSyncEvent = apps.get_model("api", "EnvironmentSyncEvent") + + sync_event = ( + EnvironmentSyncEvent.objects.filter(env_sync=environment_sync) + .order_by("-created_at") + .first() + ) + + kv_pairs = get_environment_secrets( + environment_sync.environment, environment_sync.path + ) + + if environment_sync.authentication is None: + sync_data = ( + False, + {"message": "No authentication credentials for this sync"}, + ) + raise Exception("No authentication credentials for this sync") + + project_info = environment_sync.options + + success, sync_data = sync_nomad_secrets( + kv_pairs, + environment_sync.authentication.id, + project_info.get("path"), + project_info.get("namespace"), + ) + + if success: + sync_event.status = EnvironmentSync.COMPLETED + sync_event.completed_at = timezone.now() + sync_event.meta = sync_data + sync_event.save() + + environment_sync.last_sync = timezone.now() + environment_sync.status = EnvironmentSync.COMPLETED + environment_sync.save() + + else: + sync_event.status = EnvironmentSync.FAILED + sync_event.completed_at = timezone.now() + sync_event.meta = sync_data + sync_event.save() + + environment_sync.last_sync = timezone.now() + environment_sync.status = EnvironmentSync.FAILED + environment_sync.save() + + except JobTimeoutException: + # Handle timeout exception + sync_event.status = EnvironmentSync.TIMED_OUT + sync_event.completed_at = timezone.now() + sync_event.save() + + environment_sync.last_sync = timezone.now() + environment_sync.status = EnvironmentSync.TIMED_OUT + environment_sync.save() + raise # Re-raise the JobTimeoutException + + except Exception as ex: + print(f"EXCEPTION {ex}") + sync_event.status = EnvironmentSync.FAILED + sync_event.completed_at = timezone.now() + + try: + sync_event.meta = sync_data + except: + pass + sync_event.save() + + environment_sync.last_sync = timezone.now() + environment_sync.status = EnvironmentSync.FAILED + environment_sync.save() diff --git a/backend/api/utils/syncing/nomad/main.py b/backend/api/utils/syncing/nomad/main.py new file mode 100644 index 000000000..5b8f007ec --- /dev/null +++ b/backend/api/utils/syncing/nomad/main.py @@ -0,0 +1,92 @@ +from api.utils.syncing.auth import get_credentials +import requests +import re + + +def get_nomad_token_info(credential_id): + """Get info for a given nomad token.""" + + credentials = get_credentials(credential_id) + + NOMAD_ADDR = credentials["nomad_addr"] + NOMAD_TOKEN = credentials["nomad_token_secret"] + + session = requests.Session() + session.headers.update( + { + "Authorization": f"Bearer {NOMAD_TOKEN}", + "Content-Type": "application/json", + } + ) + + url = f"{NOMAD_ADDR}/v1/acl/token/self" + response = session.get(url) + response.raise_for_status() + return response.json() + + +def test_nomad_creds(credential_id): + """Test Nomad credentials by attempting to get token info.""" + try: + get_nomad_token_info(credential_id) + return True + except requests.HTTPError as e: + return False + + +def sync_nomad_secrets(secrets, credential_id, path, namespace="default"): + results = {} + + if not secrets or len(secrets) == 0: + results["error"] = "Error: No secrets to sync." + return False, results + + try: + secrets_dict = dict(secrets) + + # Regex to validate the path + path_regex = re.compile(r"^[a-zA-Z0-9-_~/]{1,128}$") + + # Normalize and check the path + safe_path = path.strip("/").replace("//", "/") + if not path_regex.match(safe_path): + raise ValueError( + f"Invalid path: {safe_path}. Path must match the pattern [a-zA-Z0-9-_~/]{{1,128}}." + ) + + credentials = get_credentials(credential_id) + + NOMAD_ADDR = credentials["nomad_addr"] + NOMAD_TOKEN = credentials["nomad_token_secret"] + + session = requests.Session() + session.headers.update( + { + "Authorization": f"Bearer {NOMAD_TOKEN}", + "Content-Type": "application/json", + } + ) + + url = f"{NOMAD_ADDR}/v1/var/{safe_path}?namespace={namespace}" + + # All secrets are included under the 'Items' field in the payload + payload = { + "Namespace": namespace, + "Path": safe_path, + "Items": secrets_dict, + } + + response = session.put(url, json=payload) + + response.raise_for_status() + + success = True + results["message"] = ( + f"All secrets successfully synced to Nomad at path: {safe_path} in namespace: {namespace}." + ) + + except Exception as e: + success = False + results["error"] = f"An error occurred: {str(e)}" + + return success, results diff --git a/backend/backend/graphene/mutations/syncing.py b/backend/backend/graphene/mutations/syncing.py index 4b8708b7a..29c0bf45d 100644 --- a/backend/backend/graphene/mutations/syncing.py +++ b/backend/backend/graphene/mutations/syncing.py @@ -302,7 +302,7 @@ def mutate(cls, root, info, env_id, path, credential_id, engine, vault_path): for es in existing_syncs: if es.options == sync_options: - raise GraphQLError("A sync already exists for this GitHub repo!") + raise GraphQLError("A sync already exists for this Vault path!") sync = EnvironmentSync.objects.create( environment=env, @@ -317,6 +317,54 @@ def mutate(cls, root, info, env_id, path, credential_id, engine, vault_path): return CreateVaultSync(sync=sync) +class CreateNomadSync(graphene.Mutation): + class Arguments: + env_id = graphene.ID() + path = graphene.String() + credential_id = graphene.ID() + nomad_path = graphene.String() + nomad_namespace = graphene.String() + + sync = graphene.Field(EnvironmentSyncType) + + @classmethod + def mutate( + cls, root, info, env_id, path, credential_id, nomad_path, nomad_namespace + ): + service_id = "hashicorp_nomad" + service_config = ServiceConfig.get_service_config(service_id) + + env = Environment.objects.get(id=env_id) + + if not ServerEnvironmentKey.objects.filter(environment=env).exists(): + raise GraphQLError("Syncing is not enabled for this environment!") + + if not user_can_access_app(info.context.user.userId, env.app.id): + raise GraphQLError("You don't have access to this app") + + sync_options = {"path": nomad_path, "namespace": nomad_namespace} + + existing_syncs = EnvironmentSync.objects.filter( + environment__app_id=env.app.id, service=service_id, deleted_at=None + ) + + for es in existing_syncs: + if es.options == sync_options: + raise GraphQLError("A sync already exists for this Nomad path!") + + sync = EnvironmentSync.objects.create( + environment=env, + path=normalize_path_string(path), + service=service_id, + options=sync_options, + authentication_id=credential_id, + ) + + trigger_sync_tasks(sync) + + return CreateNomadSync(sync=sync) + + class DeleteSync(graphene.Mutation): class Arguments: sync_id = graphene.ID() diff --git a/backend/backend/graphene/queries/syncing.py b/backend/backend/graphene/queries/syncing.py index 8d510a68b..7fab8adb3 100644 --- a/backend/backend/graphene/queries/syncing.py +++ b/backend/backend/graphene/queries/syncing.py @@ -19,6 +19,7 @@ from api.utils.syncing.aws.secrets_manager import list_aws_secrets from api.utils.syncing.github.actions import list_repos from api.utils.syncing.vault.main import test_vault_creds +from api.utils.syncing.nomad.main import test_nomad_creds from backend.graphene.types import ProviderType, ServiceType from graphql import GraphQLError @@ -119,6 +120,14 @@ def resolve_test_vault_creds(root, info, credential_id): raise GraphQLError(f"Error testing Vault credentials: {str(ex)}") +def resolve_test_nomad_creds(root, info, credential_id): + try: + valid = test_nomad_creds(credential_id) + return valid + except Exception as ex: + raise GraphQLError(f"Error testing Nomad credentials: {str(ex)}") + + def resolve_syncs(root, info, app_id=None, env_id=None, org_id=None): # If both app_id and env_id are provided if app_id and env_id: diff --git a/backend/backend/schema.py b/backend/backend/schema.py index 1c7972800..9eeb9363e 100644 --- a/backend/backend/schema.py +++ b/backend/backend/schema.py @@ -15,6 +15,7 @@ resolve_syncs, resolve_env_syncs, resolve_test_vault_creds, + resolve_test_nomad_creds, ) from .graphene.queries.quotas import resolve_organisation_plan from .graphene.mutations.environment import ( @@ -40,6 +41,7 @@ CreateAWSSecretsManagerSync, CreateCloudflarePagesSync, CreateGitHubActionsSync, + CreateNomadSync, CreateProviderCredentials, CreateVaultSync, DeleteProviderCredentials, @@ -228,6 +230,8 @@ class Query(graphene.ObjectType): test_vault_creds = graphene.Field(graphene.Boolean, credential_id=graphene.ID()) + test_nomad_creds = graphene.Field(graphene.Boolean, credential_id=graphene.ID()) + # -------------------------------------------------------------------- resolve_server_public_key = resolve_server_public_key @@ -252,6 +256,8 @@ class Query(graphene.ObjectType): resolve_test_vault_creds = resolve_test_vault_creds + resolve_test_nomad_creds = resolve_test_nomad_creds + def resolve_organisations(root, info): memberships = OrganisationMember.objects.filter( user=info.context.user, deleted_at=None @@ -680,6 +686,9 @@ class Mutation(graphene.ObjectType): # Vault create_vault_sync = CreateVaultSync.Field() + # Nomad + create_nomad_sync = CreateNomadSync.Field() + create_user_token = CreateUserTokenMutation.Field() delete_user_token = DeleteUserTokenMutation.Field() diff --git a/frontend/apollo/gql.ts b/frontend/apollo/gql.ts index 42da756dc..7a5eec0fc 100644 --- a/frontend/apollo/gql.ts +++ b/frontend/apollo/gql.ts @@ -48,6 +48,7 @@ const documents = { "mutation DeleteSync($syncId: ID!) {\n deleteEnvSync(syncId: $syncId) {\n ok\n }\n}": types.DeleteSyncDocument, "mutation CreateNewGhActionsSync($envId: ID!, $path: String!, $repoName: String!, $owner: String!, $credentialId: ID!) {\n createGhActionsSync(\n envId: $envId\n path: $path\n repoName: $repoName\n owner: $owner\n credentialId: $credentialId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewGhActionsSyncDocument, "mutation InitAppSyncing($appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n initEnvSync(appId: $appId, envKeys: $envKeys) {\n app {\n id\n sseEnabled\n }\n }\n}": types.InitAppSyncingDocument, + "mutation CreateNewNomadSync($envId: ID!, $path: String!, $nomadPath: String!, $nomadNamespace: String!, $credentialId: ID!) {\n createNomadSync(\n envId: $envId\n path: $path\n nomadPath: $nomadPath\n nomadNamespace: $nomadNamespace\n credentialId: $credentialId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewNomadSyncDocument, "mutation SaveNewProviderCreds($orgId: ID!, $provider: String!, $name: String!, $credentials: JSONString!) {\n createProviderCredentials(\n orgId: $orgId\n provider: $provider\n name: $name\n credentials: $credentials\n ) {\n credential {\n id\n }\n }\n}": types.SaveNewProviderCredsDocument, "mutation ToggleSync($syncId: ID!) {\n toggleSyncActive(syncId: $syncId) {\n ok\n }\n}": types.ToggleSyncDocument, "mutation TriggerEnvSync($syncId: ID!) {\n triggerSync(syncId: $syncId) {\n sync {\n status\n }\n }\n}": types.TriggerEnvSyncDocument, @@ -86,6 +87,7 @@ const documents = { "query GetServerKey {\n serverPublicKey\n}": types.GetServerKeyDocument, "query GetServiceList {\n services {\n id\n name\n provider {\n id\n }\n }\n}": types.GetServiceListDocument, "query GetGithubRepos($credentialId: ID!) {\n githubRepos(credentialId: $credentialId) {\n name\n owner\n type\n }\n}": types.GetGithubReposDocument, + "query TestNomadAuth($credentialId: ID!) {\n testNomadCreds(credentialId: $credentialId)\n}": types.TestNomadAuthDocument, "query TestVaultAuth($credentialId: ID!) {\n testVaultCreds(credentialId: $credentialId)\n}": types.TestVaultAuthDocument, "query GetUserTokens($organisationId: ID!) {\n userTokens(organisationId: $organisationId) {\n id\n name\n wrappedKeyShare\n createdAt\n expiresAt\n }\n}": types.GetUserTokensDocument, }; @@ -244,6 +246,10 @@ export function graphql(source: "mutation CreateNewGhActionsSync($envId: ID!, $p * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "mutation InitAppSyncing($appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n initEnvSync(appId: $appId, envKeys: $envKeys) {\n app {\n id\n sseEnabled\n }\n }\n}"): (typeof documents)["mutation InitAppSyncing($appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n initEnvSync(appId: $appId, envKeys: $envKeys) {\n app {\n id\n sseEnabled\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateNewNomadSync($envId: ID!, $path: String!, $nomadPath: String!, $nomadNamespace: String!, $credentialId: ID!) {\n createNomadSync(\n envId: $envId\n path: $path\n nomadPath: $nomadPath\n nomadNamespace: $nomadNamespace\n credentialId: $credentialId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateNewNomadSync($envId: ID!, $path: String!, $nomadPath: String!, $nomadNamespace: String!, $credentialId: ID!) {\n createNomadSync(\n envId: $envId\n path: $path\n nomadPath: $nomadPath\n nomadNamespace: $nomadNamespace\n credentialId: $credentialId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -396,6 +402,10 @@ export function graphql(source: "query GetServiceList {\n services {\n id\n * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "query GetGithubRepos($credentialId: ID!) {\n githubRepos(credentialId: $credentialId) {\n name\n owner\n type\n }\n}"): (typeof documents)["query GetGithubRepos($credentialId: ID!) {\n githubRepos(credentialId: $credentialId) {\n name\n owner\n type\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query TestNomadAuth($credentialId: ID!) {\n testNomadCreds(credentialId: $credentialId)\n}"): (typeof documents)["query TestNomadAuth($credentialId: ID!) {\n testNomadCreds(credentialId: $credentialId)\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/apollo/graphql.ts b/frontend/apollo/graphql.ts index 467abd674..f0b6e7656 100644 --- a/frontend/apollo/graphql.ts +++ b/frontend/apollo/graphql.ts @@ -193,6 +193,11 @@ export type CreateLockboxMutation = { lockbox?: Maybe; }; +export type CreateNomadSync = { + __typename?: 'CreateNomadSync'; + sync?: Maybe; +}; + export type CreateOrganisationMemberMutation = { __typename?: 'CreateOrganisationMemberMutation'; orgMember?: Maybe; @@ -446,6 +451,7 @@ export type Mutation = { createEnvironmentToken?: Maybe; createGhActionsSync?: Maybe; createLockbox?: Maybe; + createNomadSync?: Maybe; createOrganisation?: Maybe; createOrganisationMember?: Maybe; createOverride?: Maybe; @@ -558,6 +564,15 @@ export type MutationCreateLockboxArgs = { }; +export type MutationCreateNomadSyncArgs = { + credentialId?: InputMaybe; + envId?: InputMaybe; + nomadNamespace?: InputMaybe; + nomadPath?: InputMaybe; + path?: InputMaybe; +}; + + export type MutationCreateOrganisationArgs = { id: Scalars['ID']['input']; identityKey: Scalars['String']['input']; @@ -894,6 +909,7 @@ export type Query = { services?: Maybe>>; sseEnabled?: Maybe; syncs?: Maybe>>; + testNomadCreds?: Maybe; testVaultCreds?: Maybe; userTokens?: Maybe>>; validateInvite?: Maybe; @@ -1040,6 +1056,11 @@ export type QuerySyncsArgs = { }; +export type QueryTestNomadCredsArgs = { + credentialId?: InputMaybe; +}; + + export type QueryTestVaultCredsArgs = { credentialId?: InputMaybe; }; @@ -1535,6 +1556,17 @@ export type InitAppSyncingMutationVariables = Exact<{ export type InitAppSyncingMutation = { __typename?: 'Mutation', initEnvSync?: { __typename?: 'InitEnvSync', app?: { __typename?: 'AppType', id: string, sseEnabled?: boolean | null } | null } | null }; +export type CreateNewNomadSyncMutationVariables = Exact<{ + envId: Scalars['ID']['input']; + path: Scalars['String']['input']; + nomadPath: Scalars['String']['input']; + nomadNamespace: Scalars['String']['input']; + credentialId: Scalars['ID']['input']; +}>; + + +export type CreateNewNomadSyncMutation = { __typename?: 'Mutation', createNomadSync?: { __typename?: 'CreateNomadSync', sync?: { __typename?: 'EnvironmentSyncType', id: string, isActive: boolean, lastSync?: any | null, createdAt?: any | null, environment: { __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices }, serviceInfo?: { __typename?: 'ServiceType', id?: string | null, name?: string | null } | null } | null } | null }; + export type SaveNewProviderCredsMutationVariables = Exact<{ orgId: Scalars['ID']['input']; provider: Scalars['String']['input']; @@ -1821,6 +1853,13 @@ export type GetGithubReposQueryVariables = Exact<{ export type GetGithubReposQuery = { __typename?: 'Query', githubRepos?: Array<{ __typename?: 'GitHubRepoType', name?: string | null, owner?: string | null, type?: string | null } | null> | null }; +export type TestNomadAuthQueryVariables = Exact<{ + credentialId: Scalars['ID']['input']; +}>; + + +export type TestNomadAuthQuery = { __typename?: 'Query', testNomadCreds?: boolean | null }; + export type TestVaultAuthQueryVariables = Exact<{ credentialId: Scalars['ID']['input']; }>; @@ -1871,6 +1910,7 @@ export const DeleteProviderCredsDocument = {"kind":"Document","definitions":[{"k export const DeleteSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"syncId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteEnvSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"syncId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"syncId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const CreateNewGhActionsSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewGhActionsSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"owner"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createGhActionsSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"repoName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"repoName"}}},{"kind":"Argument","name":{"kind":"Name","value":"owner"},"value":{"kind":"Variable","name":{"kind":"Name","value":"owner"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const InitAppSyncingDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InitAppSyncing"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"initEnvSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"envKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateNewNomadSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewNomadSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"nomadPath"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"nomadNamespace"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createNomadSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"nomadPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"nomadPath"}}},{"kind":"Argument","name":{"kind":"Name","value":"nomadNamespace"},"value":{"kind":"Variable","name":{"kind":"Name","value":"nomadNamespace"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const SaveNewProviderCredsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SaveNewProviderCreds"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"provider"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentials"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSONString"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createProviderCredentials"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"provider"},"value":{"kind":"Variable","name":{"kind":"Name","value":"provider"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentials"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentials"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const ToggleSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ToggleSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"syncId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toggleSyncActive"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"syncId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"syncId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const TriggerEnvSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TriggerEnvSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"syncId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"triggerSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"syncId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"syncId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode; @@ -1909,5 +1949,6 @@ export const GetSavedCredentialsDocument = {"kind":"Document","definitions":[{"k export const GetServerKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServerKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverPublicKey"}}]}}]} as unknown as DocumentNode; export const GetServiceListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServiceList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetGithubReposDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetGithubRepos"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"githubRepos"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"owner"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]} as unknown as DocumentNode; +export const TestNomadAuthDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestNomadAuth"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"testNomadCreds"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}]}]}}]} as unknown as DocumentNode; export const TestVaultAuthDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestVaultAuth"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"testVaultCreds"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}]}]}}]} as unknown as DocumentNode; export const GetUserTokensDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserTokens"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userTokens"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedKeyShare"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/apollo/schema.graphql b/frontend/apollo/schema.graphql index 3044ad4b2..6c905226b 100644 --- a/frontend/apollo/schema.graphql +++ b/frontend/apollo/schema.graphql @@ -31,6 +31,7 @@ type Query { awsSecrets(credentialId: ID): [AWSSecretType] githubRepos(credentialId: ID): [GitHubRepoType] testVaultCreds(credentialId: ID): Boolean + testNomadCreds(credentialId: ID): Boolean } type OrganisationType { @@ -481,6 +482,7 @@ type Mutation { createAwsSecretSync(credentialId: ID, envId: ID, kmsId: String, path: String, secretName: String): CreateAWSSecretsManagerSync createGhActionsSync(credentialId: ID, envId: ID, owner: String, path: String, repoName: String): CreateGitHubActionsSync createVaultSync(credentialId: ID, engine: String, envId: ID, path: String, vaultPath: String): CreateVaultSync + createNomadSync(credentialId: ID, envId: ID, nomadNamespace: String, nomadPath: String, path: String): CreateNomadSync createUserToken(expiry: BigInt, identityKey: String!, name: String!, orgId: ID!, token: String!, wrappedKeyShare: String!): CreateUserTokenMutation deleteUserToken(tokenId: ID!): DeleteUserTokenMutation createServiceToken(appId: ID!, environmentKeys: [EnvironmentKeyInput], expiry: BigInt, identityKey: String!, name: String!, token: String!, wrappedKeyShare: String!): CreateServiceTokenMutation @@ -626,6 +628,10 @@ type CreateVaultSync { sync: EnvironmentSyncType } +type CreateNomadSync { + sync: EnvironmentSyncType +} + type CreateUserTokenMutation { ok: Boolean userToken: UserTokenType diff --git a/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx b/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx index 775fd7f86..785716160 100644 --- a/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx +++ b/frontend/app/[team]/apps/[app]/environments/[environment]/[[...path]]/page.tsx @@ -706,7 +706,7 @@ export default function Environment({ } return ( -
+
{keyring !== null && !loading && (
@@ -893,19 +893,6 @@ export default function Environment({ />
))} - - {/*
- - -
*/}
)} diff --git a/frontend/components/syncing/CreateProviderCredentials.tsx b/frontend/components/syncing/CreateProviderCredentials.tsx index 467625f10..57d3ded26 100644 --- a/frontend/components/syncing/CreateProviderCredentials.tsx +++ b/frontend/components/syncing/CreateProviderCredentials.tsx @@ -97,6 +97,8 @@ export const CreateProviderCredentials = (props: { return 'https://docs.phase.dev/integrations/platforms/aws-secrets-manager' else if (provider.id === 'hashicorp_vault') return 'https://docs.phase.dev/integrations/platforms/hashicorp-vault' + else if (provider.id === 'hashicorp_nomad') + return 'https://docs.phase.dev/integrations/platforms/hashicorp-nomad' else return 'https://docs.phase.dev/integrations' } diff --git a/frontend/components/syncing/CreateSyncDialog.tsx b/frontend/components/syncing/CreateSyncDialog.tsx index 5456e3b19..a4376d189 100644 --- a/frontend/components/syncing/CreateSyncDialog.tsx +++ b/frontend/components/syncing/CreateSyncDialog.tsx @@ -7,6 +7,7 @@ import React from 'react' import { CreateAWSSecretsSync } from './AWS/CreateAWSSecretsSync' import { CreateGhActionsSync } from './GitHub/CreateGhActionsSync' import { CreateVaultSync } from './Vault/CreateVaultSync' +import { CreateNomadSync } from './Nomad/CreateNomadSync' export const CreateSyncDialog = (props: { appId: string @@ -34,6 +35,8 @@ export const CreateSyncDialog = (props: { return case 'hashicorp_vault': return + case 'hashicorp_nomad': + return default: return null diff --git a/frontend/components/syncing/Nomad/CreateNomadSync.tsx b/frontend/components/syncing/Nomad/CreateNomadSync.tsx new file mode 100644 index 000000000..11f596a56 --- /dev/null +++ b/frontend/components/syncing/Nomad/CreateNomadSync.tsx @@ -0,0 +1,205 @@ +import TestNomadAuth from '@/graphql/queries/syncing/nomad/testAuth.gql' +import GetAppSyncStatus from '@/graphql/queries/syncing/getAppSyncStatus.gql' +import GetAppEnvironments from '@/graphql/queries/secrets/getAppEnvironments.gql' +import GetSavedCredentials from '@/graphql/queries/syncing/getSavedCredentials.gql' +import CreateNewNomadSync from '@/graphql/mutations/syncing/nomad/createNomadSync.gql' +import { useLazyQuery, useMutation, useQuery } from '@apollo/client' +import { Fragment, useContext, useEffect, useState } from 'react' +import { Button } from '../../common/Button' +import { EnvironmentType, ProviderCredentialsType } from '@/apollo/graphql' +import { RadioGroup } from '@headlessui/react' +import clsx from 'clsx' +import { FaAngleDoubleDown, FaCircle, FaDotCircle } from 'react-icons/fa' +import { toast } from 'react-toastify' + +import { organisationContext } from '@/contexts/organisationContext' +import { ProviderCredentialPicker } from '../ProviderCredentialPicker' +import { ProviderIcon } from '../ProviderIcon' +import { Input } from '@/components/common/Input' + +export const CreateNomadSync = (props: { appId: string; closeModal: () => void }) => { + const { activeOrganisation: organisation } = useContext(organisationContext) + + const { appId, closeModal } = props + + const { data: appEnvsData } = useQuery(GetAppEnvironments, { + variables: { + appId, + }, + }) + const { data: credentialsData } = useQuery(GetSavedCredentials, { + variables: { orgId: organisation!.id }, + }) + + const [testCreds, { loading: credentialTestPending }] = useLazyQuery(TestNomadAuth) + + const [createNomadSync, { data: syncData, loading: creating }] = useMutation(CreateNewNomadSync) + + const [credential, setCredential] = useState(null) + + const [phaseEnv, setPhaseEnv] = useState(null) + + const [path, setPath] = useState('/') + + const [nomadPath, setNomadPath] = useState('') + const [nomadNamespace, setNomadNamespace] = useState('default') + + const [pathIsCustom, setPathIsCustom] = useState(false) + + const [credentialsValid, setCredentialsValid] = useState(false) + + useEffect(() => { + if (appEnvsData?.appEnvironments.length > 0) { + const defaultEnv: EnvironmentType = appEnvsData.appEnvironments[0] + setPhaseEnv(defaultEnv) + setNomadPath(`${defaultEnv.app.name.replace(/ /g, '-')}/${defaultEnv.name}`.toLowerCase()) + } + }, [appEnvsData]) + + useEffect(() => { + if (phaseEnv && !pathIsCustom) + setNomadPath(`${phaseEnv.app.name.replace(/ /g, '-')}/${phaseEnv.name}`.toLowerCase()) + }, [phaseEnv, pathIsCustom]) + + useEffect(() => { + if (credentialsData && credentialsData.savedCredentials.length > 0) { + setCredential(credentialsData.savedCredentials[0]) + } + }, [credentialsData]) + + const handleUpdatePath = (pathValue: string) => { + setNomadPath(pathValue) + setPathIsCustom(true) + } + + const handleSubmit = async (e: { preventDefault: () => void }) => { + e.preventDefault() + + if (credential === null) { + toast.error('Please select credential to use for this sync') + return false + } else if (!credentialsValid) { + const { data: credsTestData } = await testCreds({ + variables: { credentialId: credential.id }, + }) + if (credsTestData) { + setCredentialsValid(true) + } + } else { + await createNomadSync({ + variables: { + envId: phaseEnv?.id, + path, + nomadPath, + nomadNamespace, + credentialId: credential.id, + }, + refetchQueries: [{ query: GetAppSyncStatus, variables: { appId } }], + }) + toast.success('Created new Sync!') + closeModal() + } + } + + return ( +
+
+
+ + Hashicorp Nomad +
+
Sync an environment with Hashicorp Nomad.
+
+ +
+ {!credentialsValid && ( +
+
+ Step 1: Choose authentication credentials +
+
+
+ setCredential(cred)} + orgId={organisation!.id} + providerFilter={'hashicorp_nomad'} + setDefault={true} + /> +
+
+
+ )} + + {credentialsValid && ( +
+
+ Step 2: Select source and destination for Secrets +
+
+ + + + +
+ {appEnvsData.appEnvironments.map((env: EnvironmentType) => ( + + {({ active, checked }) => ( +
+ {checked ? : } + {env.name} +
+ )} +
+ ))} +
+
+ + +
+ +
+
+ +
+
+ +
+ + +
+
+ )} +
+
+ {credentialsValid && ( + + )} +
+ +
+
+
+ ) +} diff --git a/frontend/components/syncing/ProviderIcon.tsx b/frontend/components/syncing/ProviderIcon.tsx index 559b32b4f..d5039cb21 100644 --- a/frontend/components/syncing/ProviderIcon.tsx +++ b/frontend/components/syncing/ProviderIcon.tsx @@ -1,5 +1,5 @@ import { FaCube } from 'react-icons/fa' -import { SiAmazonaws, SiCloudflare, SiGithub, SiVault } from 'react-icons/si' +import { SiAmazonaws, SiCloudflare, SiGithub, SiNomad, SiVault } from 'react-icons/si' export const ProviderIcon = (props: { providerId: string }) => { const { providerId } = props @@ -15,5 +15,8 @@ export const ProviderIcon = (props: { providerId: string }) => { if (providerId.toLowerCase().includes('hashicorp_vault')) return + + if (providerId.toLowerCase().includes('hashicorp_nomad')) + return else return } diff --git a/frontend/components/syncing/ServiceInfo.tsx b/frontend/components/syncing/ServiceInfo.tsx index 14dfa3a2c..be0d3c93a 100644 --- a/frontend/components/syncing/ServiceInfo.tsx +++ b/frontend/components/syncing/ServiceInfo.tsx @@ -25,7 +25,7 @@ export const ServiceInfo = (props: { sync: EnvironmentSyncType }) => { {owner}/{repoName}
) - } else if (sync.serviceInfo?.id?.includes('hashicorp')) { + } else if (sync.serviceInfo?.id?.includes('hashicorp_vault')) { const engine = JSON.parse(sync.options)['engine'] const path = JSON.parse(sync.options)['path'] @@ -34,5 +34,14 @@ export const ServiceInfo = (props: { sync: EnvironmentSyncType }) => { {engine}data/{path} ) + } else if (sync.serviceInfo?.id?.includes('hashicorp_nomad')) { + const path = JSON.parse(sync.options)['path'] + const namespace = JSON.parse(sync.options)['namespace'] + + return ( +
+ {path}@{namespace || 'default'} +
+ ) } else return <>{sync.serviceInfo?.id} } diff --git a/frontend/graphql/mutations/syncing/nomad/createNomadSync.gql b/frontend/graphql/mutations/syncing/nomad/createNomadSync.gql new file mode 100644 index 000000000..e7f19acab --- /dev/null +++ b/frontend/graphql/mutations/syncing/nomad/createNomadSync.gql @@ -0,0 +1,19 @@ +mutation CreateNewNomadSync($envId: ID!, $path: String!, $nomadPath: String!, $nomadNamespace: String!, $credentialId: ID!) { + createNomadSync(envId: $envId, path: $path, nomadPath: $nomadPath, nomadNamespace: $nomadNamespace, credentialId: $credentialId) { + sync { + id + environment { + id + name + envType + } + serviceInfo { + id + name + } + isActive + lastSync + createdAt + } + } +} diff --git a/frontend/graphql/queries/syncing/nomad/testAuth.gql b/frontend/graphql/queries/syncing/nomad/testAuth.gql new file mode 100644 index 000000000..e6b47c6f8 --- /dev/null +++ b/frontend/graphql/queries/syncing/nomad/testAuth.gql @@ -0,0 +1,3 @@ +query TestNomadAuth($credentialId: ID!) { + testNomadCreds(credentialId: $credentialId) +} diff --git a/frontend/package.json b/frontend/package.json index f149e96af..64a155da1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,7 +39,7 @@ "posthog-js": "^1.104.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^5.0.1", + "react-icons": "^5.2.1", "react-toastify": "^10.0.4", "reaviz": "^15.6.1", "sass": "^1.70.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 944185b06..89bf9847a 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -8077,10 +8077,10 @@ react-icons@*: version "4.8.0" resolved "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz" -react-icons@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.0.1.tgz#1694e11bfa2a2888cab47dcc30154ce90485feee" - integrity sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw== +react-icons@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.2.1.tgz#28c2040917b2a2eda639b0f797bff1888e018e4a" + integrity sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw== react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" @@ -9618,6 +9618,7 @@ wordwrap@0.0.2: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==