Skip to content

Commit

Permalink
InstanceConfiguration API: Add /setup endpoint and GET default User…
Browse files Browse the repository at this point in the history
…/Org info (#8400)

Co-authored-by: Joey Marshment-Howell <[email protected]>
  • Loading branch information
pmossman and josephkmh committed Aug 24, 2023
1 parent 6a11335 commit dacfaff
Show file tree
Hide file tree
Showing 70 changed files with 948 additions and 448 deletions.
2 changes: 2 additions & 0 deletions airbyte-api/src/main/openapi/cloud-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,8 @@ components:
enum:
# - auth0
- google_identity_platform
- airbyte
- keycloak
# WORKSPACE
WorkspaceUserRead:
type: object
Expand Down
62 changes: 62 additions & 0 deletions airbyte-api/src/main/openapi/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3515,6 +3515,27 @@ paths:
$ref: "#/components/schemas/InstanceConfigurationResponse"
"401":
description: Fetching instance configuration failed.
/v1/instance_configuration/setup:
post:
summary: Setup an instance with user and organization information.
tags:
- instance_configuration
operationId: setupInstanceConfiguration
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/InstanceConfigurationSetupRequestBody"
responses:
"200":
description: Successfully setup instance.
content:
application/json:
schema:
$ref: "#/components/schemas/InstanceConfigurationResponse"
"401":
description: Instance setup failed.

/v1/jobs/retry_states/create_or_update:
post:
summary: Creates or updates a retry state for a job.
Expand Down Expand Up @@ -7492,6 +7513,10 @@ components:
required:
- edition
- webappUrl
- initialSetupComplete
- defaultUserId
- defaultOrganizationId
- defaultWorkspaceId
properties:
edition:
type: string
Expand All @@ -7508,6 +7533,43 @@ components:
$ref: "#/components/schemas/AuthConfiguration"
webappUrl:
type: string
initialSetupComplete:
type: boolean
defaultUserId:
type: string
format: uuid
defaultOrganizationId:
type: string
format: uuid
defaultWorkspaceId:
type: string
format: uuid
InstanceConfigurationSetupRequestBody:
type: object
required:
- workspaceId
- email
- anonymousDataCollection
- initialSetupComplete
- displaySetupWizard
properties:
workspaceId:
type: string
format: uuid
email:
type: string
anonymousDataCollection:
type: boolean
initialSetupComplete:
type: boolean
displaySetupWizard:
type: boolean
userName:
description: Optional name of the user to create. Defaults to 'Default User' if not specified.
type: string
organizationName:
description: Optional name of the organization to create. Defaults to 'Default Organization' if not specified.
type: string
StreamStatusRead:
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.airbyte.config.StandardWorkspace;
import io.airbyte.config.init.PostLoadExecutor;
import io.airbyte.config.persistence.ConfigRepository;
import io.airbyte.config.persistence.OrganizationPersistence;
import io.airbyte.db.init.DatabaseInitializationException;
import io.airbyte.db.init.DatabaseInitializer;
import io.airbyte.db.instance.DatabaseMigrator;
Expand Down Expand Up @@ -188,7 +189,9 @@ private void createWorkspaceIfNoneExists(final ConfigRepository configRepository
.withInitialSetupComplete(false)
.withDisplaySetupWizard(true)
.withTombstone(false)
.withDefaultGeography(Geography.AUTO);
.withDefaultGeography(Geography.AUTO)
// attach this new workspace to the Default Organization which should always exist at this point.
.withOrganizationId(OrganizationPersistence.DEFAULT_ORGANIZATION_ID);
// NOTE: it's safe to use the NoSecrets version since we know that the user hasn't supplied any
// secrets yet.
configRepository.writeStandardWorkspaceNoSecrets(workspace);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

import io.airbyte.commons.resources.MoreResources;
import io.airbyte.config.persistence.ConfigRepository;
import io.airbyte.config.persistence.OrganizationPersistence;
import io.airbyte.config.persistence.UserPersistence;
import io.airbyte.config.persistence.WorkspacePersistence;
import io.airbyte.db.Database;
import io.airbyte.db.check.impl.JobsDatabaseAvailabilityCheck;
import io.airbyte.db.factory.DatabaseCheckFactory;
Expand Down Expand Up @@ -147,4 +150,19 @@ public DatabaseMigrator jobsDatabaseMigrator(@Named("jobsDatabase") final Databa
return new JobsDatabaseMigrator(jobsDatabase, jobsFlyway);
}

@Singleton
public UserPersistence userPersistence(@Named("configDatabase") final Database configDatabase) {
return new UserPersistence(configDatabase);
}

@Singleton
public OrganizationPersistence organizationPersistence(@Named("configDatabase") final Database configDatabase) {
return new OrganizationPersistence(configDatabase);
}

@Singleton
public WorkspacePersistence workspacePersistence(@Named("configDatabase") final Database configDatabase) {
return new WorkspacePersistence(configDatabase);
}

}
2 changes: 2 additions & 0 deletions airbyte-commons-license/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ dependencies {
annotationProcessor libs.lombok

implementation project(':airbyte-commons')
implementation project(':airbyte-commons-micronaut')
implementation project(':airbyte-config:config-models')

testAnnotationProcessor platform(libs.micronaut.bom)
testAnnotationProcessor libs.bundles.micronaut.test.annotation.processor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
@Requires(condition = AirbyteProEnabledCondition.class)
public @interface RequiresAirbyteProEnabled {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package io.airbyte.commons.license.condition;

import io.airbyte.config.Configs.AirbyteEdition;
import io.micronaut.context.condition.Condition;
import io.micronaut.context.condition.ConditionContext;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -18,10 +19,8 @@ public class AirbyteProEnabledCondition implements Condition {

@Override
public boolean matches(ConditionContext context) {
log.warn("inside the pro enabled condition!");
final var edition = context.getProperty("airbyte.edition", String.class).orElse("community");
log.warn("got edition: " + edition);
return "pro".equals(edition);
final AirbyteEdition edition = context.getBean(AirbyteEdition.class);
return edition.equals(AirbyteEdition.PRO);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package io.airbyte.micronaut.config;

import io.airbyte.commons.version.AirbyteVersion;
import io.airbyte.config.Configs.AirbyteEdition;
import io.airbyte.config.Configs.DeploymentMode;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Value;
Expand Down Expand Up @@ -33,4 +34,12 @@ public DeploymentMode deploymentMode(@Value("${airbyte.deployment-mode}") final
return convertToEnum(deploymentMode, DeploymentMode::valueOf, DeploymentMode.OSS);
}

/**
* Fetch the configured edition of the Airbyte instance. Defaults to COMMUNITY.
*/
@Singleton
public AirbyteEdition airbyteEdition(@Value("${airbyte.edition:COMMUNITY}") final String airbyteEdition) {
return convertToEnum(airbyteEdition.toUpperCase(), AirbyteEdition::valueOf, AirbyteEdition.COMMUNITY);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright (c) 2023 Airbyte, Inc., all rights reserved.
*/

package io.airbyte.commons.server.handlers;

import io.airbyte.api.model.generated.AuthConfiguration;
import io.airbyte.api.model.generated.InstanceConfigurationResponse;
import io.airbyte.api.model.generated.InstanceConfigurationResponse.EditionEnum;
import io.airbyte.api.model.generated.InstanceConfigurationResponse.LicenseTypeEnum;
import io.airbyte.api.model.generated.InstanceConfigurationSetupRequestBody;
import io.airbyte.api.model.generated.WorkspaceUpdate;
import io.airbyte.commons.auth.config.AirbyteKeycloakConfiguration;
import io.airbyte.commons.enums.Enums;
import io.airbyte.commons.license.ActiveAirbyteLicense;
import io.airbyte.config.Configs.AirbyteEdition;
import io.airbyte.config.Organization;
import io.airbyte.config.StandardWorkspace;
import io.airbyte.config.User;
import io.airbyte.config.persistence.ConfigNotFoundException;
import io.airbyte.config.persistence.ConfigRepository;
import io.airbyte.config.persistence.OrganizationPersistence;
import io.airbyte.config.persistence.UserPersistence;
import io.airbyte.validation.json.JsonValidationException;
import io.micronaut.context.annotation.Value;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;

/**
* InstanceConfigurationHandler. Javadocs suppressed because api docs should be used as source of
* truth.
*/
@SuppressWarnings("MissingJavadocMethod")
@Slf4j
@Singleton
public class InstanceConfigurationHandler {

private final String webappUrl;
private final AirbyteEdition airbyteEdition;
private final Optional<AirbyteKeycloakConfiguration> airbyteKeycloakConfiguration;
private final Optional<ActiveAirbyteLicense> activeAirbyteLicense;
private final ConfigRepository configRepository;
private final WorkspacesHandler workspacesHandler;
private final UserPersistence userPersistence;
private final OrganizationPersistence organizationPersistence;

// the injected webapp-url value defaults to `null` to preserve backwards compatibility.
// TODO remove the default value once configurations are standardized to always include a
// webapp-url.
public InstanceConfigurationHandler(@Value("${airbyte.webapp-url:null}") final String webappUrl,
final AirbyteEdition airbyteEdition,
final Optional<AirbyteKeycloakConfiguration> airbyteKeycloakConfiguration,
final Optional<ActiveAirbyteLicense> activeAirbyteLicense,
final ConfigRepository configRepository,
final WorkspacesHandler workspacesHandler,
final UserPersistence userPersistence,
final OrganizationPersistence organizationPersistence) {
this.webappUrl = webappUrl;
this.airbyteEdition = airbyteEdition;
this.airbyteKeycloakConfiguration = airbyteKeycloakConfiguration;
this.activeAirbyteLicense = activeAirbyteLicense;
this.configRepository = configRepository;
this.workspacesHandler = workspacesHandler;
this.userPersistence = userPersistence;
this.organizationPersistence = organizationPersistence;
}

public InstanceConfigurationResponse getInstanceConfiguration() throws IOException, ConfigNotFoundException {
final StandardWorkspace defaultWorkspace = getDefaultWorkspace();

return new InstanceConfigurationResponse()
.webappUrl(webappUrl)
.edition(Enums.convertTo(airbyteEdition, EditionEnum.class))
.licenseType(getLicenseType())
.auth(getAuthConfiguration())
.initialSetupComplete(defaultWorkspace.getInitialSetupComplete())
.defaultUserId(getDefaultUserId())
.defaultOrganizationId(getDefaultOrganizationId())
.defaultWorkspaceId(defaultWorkspace.getWorkspaceId());
}

public InstanceConfigurationResponse setupInstanceConfiguration(final InstanceConfigurationSetupRequestBody requestBody)
throws IOException, JsonValidationException, ConfigNotFoundException {

// Update the default organization and user with the provided information
updateDefaultOrganization(requestBody);
updateDefaultUser(requestBody);

// Update the underlying workspace to mark the initial setup as complete
workspacesHandler.updateWorkspace(new WorkspaceUpdate()
.workspaceId(requestBody.getWorkspaceId())
.email(requestBody.getEmail())
.displaySetupWizard(requestBody.getDisplaySetupWizard())
.anonymousDataCollection(requestBody.getAnonymousDataCollection())
.initialSetupComplete(requestBody.getInitialSetupComplete()));

// Return the updated instance configuration
return getInstanceConfiguration();
}

private LicenseTypeEnum getLicenseType() {
if (airbyteEdition.equals(AirbyteEdition.PRO) && activeAirbyteLicense.isPresent()) {
return Enums.convertTo(activeAirbyteLicense.get().getLicenseType(), LicenseTypeEnum.class);
} else {
return null;
}
}

private AuthConfiguration getAuthConfiguration() {
if (airbyteEdition.equals(AirbyteEdition.PRO) && airbyteKeycloakConfiguration.isPresent()) {
return new AuthConfiguration()
.clientId(airbyteKeycloakConfiguration.get().getWebClientId())
.defaultRealm(airbyteKeycloakConfiguration.get().getAirbyteRealm());
} else {
return null;
}
}

private UUID getDefaultUserId() throws IOException {
return userPersistence.getDefaultUser().orElseThrow(() -> new IllegalStateException("Default user does not exist.")).getUserId();
}

private void updateDefaultUser(final InstanceConfigurationSetupRequestBody requestBody) throws IOException {
final User defaultUser = userPersistence.getDefaultUser().orElseThrow(() -> new IllegalStateException("Default user does not exist."));
// email is a required request property, so always set it.
defaultUser.setEmail(requestBody.getEmail());

// name is currently optional, so only set it if it is provided.
if (requestBody.getUserName() != null) {
defaultUser.setName(requestBody.getUserName());
}

userPersistence.writeUser(defaultUser);
}

private UUID getDefaultOrganizationId() throws IOException, ConfigNotFoundException {
return organizationPersistence.getDefaultOrganization()
.orElseThrow(() -> new IllegalStateException("Default organization does not exist."))
.getOrganizationId();
}

private void updateDefaultOrganization(final InstanceConfigurationSetupRequestBody requestBody) throws IOException {
final Organization defaultOrganization =
organizationPersistence.getDefaultOrganization().orElseThrow(() -> new IllegalStateException("Default organization does not exist."));

// email is a required request property, so always set it.
defaultOrganization.setEmail(requestBody.getEmail());

// name is currently optional, so only set it if it is provided.
if (requestBody.getOrganizationName() != null) {
defaultOrganization.setName(requestBody.getOrganizationName());
}

organizationPersistence.updateOrganization(defaultOrganization);
}

// Currently, the default workspace is simply the first workspace created by the bootloader. This is
// hacky, but
// historically, the first workspace is used to store instance-level preferences.
// TODO introduce a proper means of persisting instance-level preferences instead of using the first
// workspace as a proxy.
private StandardWorkspace getDefaultWorkspace() throws IOException {
return configRepository.listStandardWorkspaces(true).stream().findFirst()
.orElseThrow(() -> new IllegalStateException("Default workspace does not exist."));
}

}
Loading

0 comments on commit dacfaff

Please sign in to comment.