Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support in the sidecar for feature flags #76

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<avro.version>1.12.0</avro.version>
<jackson-dataformat.version>2.17.1</jackson-dataformat.version>
<versions-maven-plugin.version>2.8.1</versions-maven-plugin.version>
<launch-darkly-sdk.version>7.0.0</launch-darkly-sdk.version>
</properties>

<repositories>
Expand Down Expand Up @@ -203,6 +204,11 @@
<artifactId>jackson-dataformat-avro</artifactId>
<version>${jackson-dataformat.version}</version>
</dependency>
<dependency>
<groupId>com.launchdarkly</groupId>
<artifactId>launchdarkly-java-server-sdk</artifactId>
<version>${launch-darkly-sdk.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
Expand Down
58 changes: 58 additions & 0 deletions src/generated/resources/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"description" : "Auto generated value"
} ],
"tags" : [ {
"name" : "Feature Flags",
"description" : "Feature flags"
}, {
"name" : "Templates",
"description" : "Code generation templates"
} ],
Expand Down Expand Up @@ -256,6 +259,61 @@
}
}
},
"/gateway/v1/feature-flags/{id}/value" : {
"get" : {
"tags" : [ "Feature Flags" ],
"parameters" : [ {
"name" : "id",
"in" : "path",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"200" : {
"description" : "OK",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/JsonNode"
}
}
}
},
"401" : {
"description" : "Not Authorized",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Failure"
}
}
}
},
"404" : {
"description" : "Feature flag not found",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Failure"
}
}
}
},
"500" : {
"description" : "Internal error parsing feature flags",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Failure"
}
}
}
}
}
}
},
"/gateway/v1/handshake" : {
"get" : {
"tags" : [ "Handshake Resource" ],
Expand Down
37 changes: 37 additions & 0 deletions src/generated/resources/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ servers:
- url: http://127.0.0.1:26636
description: Auto generated value
tags:
- name: Feature Flags
description: Feature flags
- name: Templates
description: Code generation templates
paths:
Expand Down Expand Up @@ -172,6 +174,41 @@ paths:
responses:
"204":
description: No Content
/gateway/v1/feature-flags/{id}/value:
get:
tags:
- Feature Flags
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/JsonNode"
"401":
description: Not Authorized
content:
application/json:
schema:
$ref: "#/components/schemas/Failure"
"404":
description: Feature flag not found
content:
application/json:
schema:
$ref: "#/components/schemas/Failure"
"500":
description: Internal error parsing feature flags
content:
application/json:
schema:
$ref: "#/components/schemas/Failure"
/gateway/v1/handshake:
get:
tags:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright [2024 - 2024] Confluent Inc.
*/

package io.confluent.idesidecar.restapi.application;

import io.confluent.idesidecar.restapi.util.OperatingSystemType;
import io.confluent.idesidecar.restapi.util.OperatingSystemType.Properties;
import io.quarkus.logging.Log;
import io.quarkus.runtime.Startup;
import io.smallrye.common.constraint.NotNull;
import jakarta.inject.Singleton;
import java.util.Optional;
import java.util.regex.Pattern;
import org.eclipse.microprofile.config.ConfigProvider;

/**
* General information about the sidecar, including its version, OS information, and
* VS Code information (if available).
*
* <p>The OS information is obtained from the {@link System#getProperty system properties}:
* <ul>
* <li>{@link #osName()} -- the value of the '{@value #OS_NAME_KEY}' system property</li>
* <li>{@link #osVersion()} -- the value of the '{@value #OS_VERSION_KEY}' system property</li>
* <li>{@link #osType()} -- enumeration derived from the {@value #OS_NAME_KEY} system property
* </ul>
*
* <p>The VS Code information is obtained first from the system properties if
* <ul>
* <li>{@link VsCode#version()} -- the value of the '{@value #VSCODE_VERSION_KEY}' system property
* (e.g., {@code -Dvscode.version=0.17.1}, or if not defined from the
* {@value #VSCODE_VERSION_ENV} environment variable</li>
* <li>{@link VsCode#extensionVersion()} ()} -- the value of the '{@value #VSCODE_VERSION_KEY}'
* system property (e.g., {@code -Dvscode.extension.version=0.17.1}, or if not defined
* from the {@value #VSCODE_EXTENSION_VERSION_ENV} environment variable</li>
* </ul>
*/
@Startup
@Singleton
flippingbits marked this conversation as resolved.
Show resolved Hide resolved
public class SidecarInfo {

/* UNSET and VERSION patterned after how determined in ...application.Main */
static final String UNSET_VERSION = "unset";

static final String VERSION = ConfigProvider
.getConfig()
.getOptionalValue("quarkus.application.version", String.class)
.orElse(UNSET_VERSION);

public record VsCode(
String version,
String extensionVersion
) {
}

static final Pattern SEMANTIC_VERSION_FROM = Pattern.compile("(\\d+[.]\\d+([.]\\d+)?)");

static String semanticVersionWithin(Properties props, String key, String def) {
var value = props.getProperty(key, def);
if (value == null) {
return null;
}
var matcher = SEMANTIC_VERSION_FROM.matcher(value);
return matcher.find() ? matcher.group(1) : value;
}

static final String OS_NAME_KEY = "os.name";
static final String OS_VERSION_KEY = "os.version";
static final String VSCODE_VERSION_ENV = "VSCODE_VERSION";
static final String VSCODE_VERSION_KEY = "vscode.version";
static final String VSCODE_EXTENSION_VERSION_ENV = "VSCODE_EXTENSION_VERSION";
static final String VSCODE_EXTENSION_VERSION_KEY = "vscode.extension.version";

private final OperatingSystemType osType;
private final String osName;
private final String osVersion;
private final Optional<VsCode> vscode;

public SidecarInfo() {
this(
System::getProperty,
(key, def) -> {
var result = System.getenv(key);
return result != null ? result : def;
}
);
}

SidecarInfo(@NotNull Properties system, @NotNull Properties env) {

// Get the OS information
osName = system.getProperty(OS_NAME_KEY, "unknown");
osVersion = system.getProperty(OS_VERSION_KEY, "unknown");

// Determine the best-matching OS type
osType = OperatingSystemType.from(system);

// Set the VS Code information if available
var vscodeVersion = semanticVersionWithin(system, VSCODE_VERSION_KEY, null);
if (vscodeVersion == null) {
vscodeVersion = semanticVersionWithin(env, VSCODE_VERSION_ENV, null);
}
var vscodeExtensionVersion = semanticVersionWithin(system, VSCODE_EXTENSION_VERSION_KEY, null);
if (vscodeExtensionVersion == null) {
vscodeExtensionVersion = semanticVersionWithin(env, VSCODE_EXTENSION_VERSION_ENV, null);
}
if (vscodeVersion != null) {
vscode = Optional.of(new VsCode(vscodeVersion, vscodeExtensionVersion));
} else {
vscode = Optional.empty();
}

Log.info(this);
}

public String version() {
return VERSION;
}

public OperatingSystemType osType() {
return osType;
}

public String osName() {
return osName;
}

public String osVersion() {
return osVersion;
}

public Optional<VsCode> vsCode() {
return vscode;
}

@Override
public String toString() {
return "OS: %s %s (%s); VS Code %s, extension version %s".formatted(
osName,
osVersion,
osType.name(),
vsCode().map(VsCode::version).orElse("unknown"),
vsCode().map(VsCode::extensionVersion).orElse("unknown")
);
}

static String getSystemOrEnvProperty(String name) {
var result = System.getProperty(name);
return result != null ? result : System.getenv(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,35 @@ public Response mapTemplateNotFoundException(
.build();
}

@ServerExceptionMapper
public Response mapFlagNotFoundException(
FlagNotFoundException exception
) {

Failure failure = new Failure(
exception,
Status.NOT_FOUND,
"resource_missing", exception.getMessage(), uuidFactory.getRandomUuid(),
null);
return Response
.status(Status.NOT_FOUND)
.entity(failure)
// Explicitly set the content type to JSON here
// since the resource method may have set it to something else
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.build();
}

@ServerExceptionMapper
public Response mapGenericTemplateRegistryException(TemplateRegistryException exception) {
Failure failure = new Failure(
exception,
Status.INTERNAL_SERVER_ERROR,
exception.getCode(), Status.INTERNAL_SERVER_ERROR.getReasonPhrase(),
exception.getCode(),
Status.INTERNAL_SERVER_ERROR.getReasonPhrase(),
uuidFactory.getRandomUuid(),
null);
null
);
return Response
.status(Status.INTERNAL_SERVER_ERROR)
.entity(failure)
Expand Down Expand Up @@ -108,9 +128,11 @@ public Response mapTemplateRegistryIOException(
Failure failure = new Failure(
exception,
Status.INTERNAL_SERVER_ERROR,
exception.getCode(), Status.INTERNAL_SERVER_ERROR.getReasonPhrase(),
exception.getCode(),
Status.INTERNAL_SERVER_ERROR.getReasonPhrase(),
uuidFactory.getRandomUuid(),
null);
null
);
return Response
.status(Status.INTERNAL_SERVER_ERROR)
.entity(failure)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.confluent.idesidecar.restapi.exceptions;

import io.quarkus.runtime.annotations.RegisterForReflection;

/**
* Exception thrown when there is an error fetching feature flags
*/
@RegisterForReflection
public class FeatureFlagFailureException extends RuntimeException {

private final String code;

public FeatureFlagFailureException(String message, String code, Throwable t) {
super(message, t);
this.code = code;
}

public FeatureFlagFailureException(Throwable t) {
super(t);
this.code = null;
}

public String getCode() {
return code;
}
}
Loading