From 64569ab44b19d9ff6cd79882c45e3055c7aee22e Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 18 Mar 2025 02:27:42 +0800 Subject: [PATCH] feat: added bootstrap to CLI --- backend/src/server/routes/v1/admin-router.ts | 19 ++-- .../super-admin/super-admin-service.ts | 8 +- .../services/super-admin/super-admin-types.ts | 2 +- cli/packages/api/api.go | 20 +++++ cli/packages/api/model.go | 24 +++++ cli/packages/cmd/bootstrap.go | 90 +++++++++++++++++++ 6 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 cli/packages/cmd/bootstrap.go diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index f46c2f2dd..b0d474be9 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -401,22 +401,22 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/initialize", + url: "/bootstrap", config: { rateLimit: writeLimit }, schema: { body: z.object({ - email: z.string().email().trim(), - password: z.string().trim(), - organizationName: z.string().trim() + email: z.string().email().trim().min(1), + password: z.string().trim().min(1), + organization: z.string().trim().min(1) }), response: { 200: z.object({ message: z.string(), user: UsersSchema, organization: OrganizationsSchema, - machineIdentity: IdentitiesSchema.extend({ + identity: IdentitiesSchema.extend({ credentials: z.object({ token: z.string() }) // would just be Token AUTH for now @@ -425,8 +425,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { user, organization, machineIdentity } = await server.services.superAdmin.initializeInstance({ - ...req.body + const { user, organization, machineIdentity } = await server.services.superAdmin.bootstrapInstance({ + ...req.body, + organizationName: req.body.organization }); await server.services.telemetry.sendPostHogEvents({ @@ -441,10 +442,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); return { - message: "Successfully initialized instance", + message: "Successfully boostrapped instance", user: user.user, organization, - machineIdentity + identity: machineIdentity }; } }); diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 2164ca146..1f1ed592d 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -27,9 +27,9 @@ import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; import { LoginMethod, + TAdminBoostrapInstanceDTO, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, - TAdminInitializeInstanceDTO, TAdminSignUpDTO } from "./super-admin-types"; @@ -291,7 +291,7 @@ export const superAdminServiceFactory = ({ return { token, user: userInfo, organization }; }; - const initializeInstance = async ({ email, password, organizationName }: TAdminInitializeInstanceDTO) => { + const bootstrapInstance = async ({ email, password, organizationName }: TAdminBoostrapInstanceDTO) => { const appCfg = getConfig(); const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); if (serverCfg?.initialized) { @@ -352,7 +352,7 @@ export const superAdminServiceFactory = ({ }); const { identity, credentials } = await identityDAL.transaction(async (tx) => { - const newIdentity = await identityDAL.create({ name: "Admin Identity" }, tx); + const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx); await identityOrgMembershipDAL.create( { identityId: newIdentity.id, @@ -547,7 +547,7 @@ export const superAdminServiceFactory = ({ initServerCfg, updateServerCfg, adminSignUp, - initializeInstance, + bootstrapInstance, getUsers, deleteUser, getIdentities, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index ad989fcf4..49d48e464 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -16,7 +16,7 @@ export type TAdminSignUpDTO = { userAgent: string; }; -export type TAdminInitializeInstanceDTO = { +export type TAdminBoostrapInstanceDTO = { email: string; password: string; organizationName: string; diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 454257405..04fd8819e 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -600,3 +600,23 @@ func CallGatewayHeartBeatV1(httpClient *resty.Client) error { return nil } + +func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (*BootstrapInstanceResponse, error) { + var resBody BootstrapInstanceResponse + response, err := httpClient. + R(). + SetResult(&resBody). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain)) + + if err != nil { + return nil, fmt.Errorf("CallBootstrapInstance: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return nil, fmt.Errorf("CallBootstrapInstance: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return &resBody, nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 1eb936f24..dba6bc3c7 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -654,3 +654,27 @@ type ExchangeRelayCertResponseV1 struct { Certificate string `json:"certificate"` CertificateChain string `json:"certificateChain"` } + +type BootstrapInstanceRequest struct { + Email string `json:"email"` + Password string `json:"password"` + Organization string `json:"organization"` + Domain string `json:"domain"` +} + +type BootstrapInstanceResponseOrganization struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type BootstrapInstanceResponseIdentity struct { + ID string `json:"id"` + Name string `json:"name"` + Credentials interface{} `json:"credentials"` +} + +type BootstrapInstanceResponse struct { + Message string `json:"message"` + Organization BootstrapInstanceResponseOrganization `json:"organization"` + Identity BootstrapInstanceResponseIdentity `json:"identity"` +} diff --git a/cli/packages/cmd/bootstrap.go b/cli/packages/cmd/bootstrap.go new file mode 100644 index 000000000..c91ee75d9 --- /dev/null +++ b/cli/packages/cmd/bootstrap.go @@ -0,0 +1,90 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var bootstrapCmd = &cobra.Command{ + Use: "bootstrap", + Short: "Used to bootstrap your Infisical instance", + DisableFlagsInUseLine: true, + Example: "infisical bootstrap", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + email, _ := cmd.Flags().GetString("email") + if email == "" { + if envEmail, ok := os.LookupEnv("INFISICAL_ADMIN_EMAIL"); ok { + email = envEmail + } + } + + if email == "" { + log.Error().Msg("email is required") + return + } + + password, _ := cmd.Flags().GetString("password") + if password == "" { + if envPassword, ok := os.LookupEnv("INFISICAL_ADMIN_PASSWORD"); ok { + password = envPassword + } + } + + if password == "" { + log.Error().Msg("password is required") + return + } + + organization, _ := cmd.Flags().GetString("organization") + if organization == "" { + log.Error().Msg("organization is required") + return + } + + domain, _ := cmd.Flags().GetString("domain") + if domain == "" { + log.Error().Msg("domain is required") + return + } + + httpClient := resty.New(). + SetHeader("Accept", "application/json") + + bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{ + Domain: util.AppendAPIEndpoint(domain), + Email: email, + Password: password, + Organization: organization, + }) + + if err != nil { + log.Error().Msgf("Failed to bootstrap instance: %v", err) + } + + responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ") + if err != nil { + log.Fatal().Msgf("Failed to convert response to JSON: %v", err) + } + fmt.Println(string(responseJSON)) + }, +} + +func init() { + bootstrapCmd.Flags().String("domain", "", "The domain of your self-hosted Infisical instance") + bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin") + bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin") + bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance") + + rootCmd.AddCommand(bootstrapCmd) +}