diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b9272e..6d0555a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,7 @@ jobs: rm interface-application.yaml sed -i -e "/###APP_CONFIG###/r interface-app-config.yaml" -e "//d" interface.yaml rm interface-app-config.yaml - zip -r ./appstackfor${{ inputs.type }}.zip . -x "*.git*" -x "java/*" -x "test/*" -x "images/*" -x "listing/*" -x ".github/*" -x "*.md" -x "troubleshooting/*" -x "tutorials/*" -x "screenshots/*" -x "*.md" + zip -r ./appstackfor${{ inputs.type }}.zip . -x "*.git*" -x "java/*" -x "dotnet/*" -x "test/*" -x "images/*" -x "listing/*" -x ".github/*" -x "*.md" -x "troubleshooting/*" -x "tutorials/*" -x "screenshots/*" -x "*.md" ls -lai - name: upload-artifact uses: actions/upload-artifact@v3 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a91c6f5..c1b80de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ on: type: description: Stack type required: true - default: 'main' + default: 'java' type: choice options: - java diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml index 7d444ae..e87fcf0 100644 --- a/.github/workflows/on-push.yml +++ b/.github/workflows/on-push.yml @@ -5,4 +5,4 @@ jobs: uses: oracle-quickstart/appstack/.github/workflows/build.yml@main with: branch: ${{github.ref_name}} - type: 'java' + type: 'dotnet' diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 39e813f..b332bc0 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -17,7 +17,7 @@ jobs: uses: ./.github/workflows/build.yml with: branch: ${{github.ref_name}} - type: 'java' + type: 'dotnet' artifact-prefix: ${{github.sha}}_ run-test: needs: call-workflow-passing-data @@ -44,8 +44,8 @@ jobs: OCI_USER_OCID: ${{ secrets.OCI_USER_OCID }} OCI_PRIVATE_KEY_PEM: ${{ secrets.OCI_PRIVATE_KEY_PEM }} OCI_FINGERPRINT: ${{ secrets.OCI_FINGERPRINT }} - OCI_TEST_INPUT_JAVA: ${{ secrets.OCI_TEST_INPUT_JAVA }} + OCI_TEST_INPUT_DOTNET: ${{ secrets.OCI_TEST_INPUT_DOTNET }} run: | cd test - echo $OCI_TEST_INPUT_JAVA > input-${{ inputs.type }}.json + echo $OCI_TEST_INPUT_DOTNET > input-${{ inputs.type }}.json java -jar appstack-test.jar appstackfor${{ inputs.type }}.zip input-${{ inputs.type }}.json diff --git a/.gitignore b/.gitignore index 496ee2c..7cc3c0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.DS_Store \ No newline at end of file +.DS_Store +appstack-test/target/** +appstack-test/.vscode/** diff --git a/appstack-test/pom.xml b/appstack-test/pom.xml new file mode 100644 index 0000000..8313111 --- /dev/null +++ b/appstack-test/pom.xml @@ -0,0 +1,124 @@ + + + + 4.0.0 + + oracle.appstack + appstack-test + 1.0-SNAPSHOT + + appstack-test + + http://www.example.com + + + UTF-8 + 3.25.2 + + + + + com.oracle.oci.sdk + oci-java-sdk-common + ${oci.version} + + + com.oracle.oci.sdk + oci-java-sdk-core + ${oci.version} + + + com.oracle.oci.sdk + oci-java-sdk-resourcemanager + ${oci.version} + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey + ${oci.version} + + + com.oracle.oci.sdk + oci-java-sdk-artifacts + ${oci.version} + + + jakarta.json + jakarta.json-api + 2.1.2 + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + 11 + + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + + + jar-with-dependencies + + + + oracle.appstack.App + + + + + + + diff --git a/appstack-test/src/main/java/oracle/appstack/App.java b/appstack-test/src/main/java/oracle/appstack/App.java new file mode 100644 index 0000000..a9e9da5 --- /dev/null +++ b/appstack-test/src/main/java/oracle/appstack/App.java @@ -0,0 +1,32 @@ +package oracle.appstack; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Base64; + +public class App { + public static void main(String[] args) { + if (args.length != 2) { + System.out.println("Wrong number of parameter."); + System.exit(-2); + } + try (FileInputStream zipFileInputStream = new FileInputStream( + args[0])) { + String testInput = args[1]; + byte[] bytes = zipFileInputStream.readAllBytes(); + String zipFileBase64Encoded = Base64.getEncoder().encodeToString(bytes); + + TestRunner testRunner = new TestRunner(zipFileBase64Encoded); + String deployResult = testRunner.runTestSuite(testInput); + System.out.println(deployResult); + if (deployResult != "SUCCEDED") { + System.exit(-1); + } + + } catch (IOException ex) { + ex.printStackTrace(); + System.exit(-1); + } + + } +} \ No newline at end of file diff --git a/appstack-test/src/main/java/oracle/appstack/TestInput.java b/appstack-test/src/main/java/oracle/appstack/TestInput.java new file mode 100644 index 0000000..9f5b79d --- /dev/null +++ b/appstack-test/src/main/java/oracle/appstack/TestInput.java @@ -0,0 +1,55 @@ +package oracle.appstack; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import jakarta.json.JsonValue.ValueType; +import jakarta.json.JsonString; + +public class TestInput { + private Map variables; + private List testUrls; + private String testName; + + private TestInput() { + variables = new HashMap<>(); + testUrls = new ArrayList<>(); + } + + public Map getVariables() { + return variables; + } + + public List getTestUrls() { + return testUrls; + } + + public String getTestName() { + return this.testName; + } + + public static TestInput fromJsonObject(JsonObject jsonObject) { + TestInput testInput = new TestInput(); + if (jsonObject.containsKey("test-name")) { + testInput.testName = jsonObject.getString("test-name"); + } + if (jsonObject.containsKey("variables")) { + JsonObject variables = jsonObject.getJsonObject("variables"); + for (String key : variables.keySet()) { + testInput.getVariables().put(key, variables.getString(key)); + } + } + if (jsonObject.containsKey("test_urls")) { + for (JsonValue item : jsonObject.getJsonArray("test_urls")) { + if (item.getValueType() == ValueType.STRING) + testInput.getTestUrls().add(((JsonString) item).getString()); + } + } + return testInput; + } + +} \ No newline at end of file diff --git a/appstack-test/src/main/java/oracle/appstack/TestInputList.java b/appstack-test/src/main/java/oracle/appstack/TestInputList.java new file mode 100644 index 0000000..600fb29 --- /dev/null +++ b/appstack-test/src/main/java/oracle/appstack/TestInputList.java @@ -0,0 +1,43 @@ +package oracle.appstack; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import jakarta.json.Json; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParser.Event; + +public class TestInputList { + + private List input; + + private TestInputList() { + input = new ArrayList<>(); + } + + public static TestInputList fromJsonFile(String fileName) throws IOException { + TestInputList testInputList = new TestInputList(); + try (FileInputStream fileInputStream = new FileInputStream(fileName); + JsonParser jsonParser = Json.createParser(fileInputStream)) { + if (jsonParser.hasNext()) { + if (jsonParser.next() == Event.START_ARRAY) { + for (JsonValue item : jsonParser.getArray()) { + TestInput testInput = TestInput.fromJsonObject(item.asJsonObject()); + testInputList.input.add(testInput); + } + } + } + return testInputList; + } catch (IOException ex) { + throw ex; + } + } + + public List getTestInputList() { + return this.input; + } + +} diff --git a/appstack-test/src/main/java/oracle/appstack/TestRunner.java b/appstack-test/src/main/java/oracle/appstack/TestRunner.java new file mode 100644 index 0000000..4b7ea84 --- /dev/null +++ b/appstack-test/src/main/java/oracle/appstack/TestRunner.java @@ -0,0 +1,338 @@ +package oracle.appstack; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import com.oracle.bmc.Region; +import com.oracle.bmc.artifacts.ArtifactsClient; +import com.oracle.bmc.artifacts.model.GenericArtifactSummary; +import com.oracle.bmc.artifacts.requests.DeleteGenericArtifactRequest; +import com.oracle.bmc.artifacts.requests.ListGenericArtifactsRequest; +import com.oracle.bmc.artifacts.responses.DeleteGenericArtifactResponse; +import com.oracle.bmc.artifacts.responses.ListGenericArtifactsResponse; +import com.oracle.bmc.auth.AuthenticationDetailsProvider; +import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider; +import com.oracle.bmc.auth.StringPrivateKeySupplier; +import com.oracle.bmc.resourcemanager.ResourceManagerClient; +import com.oracle.bmc.resourcemanager.model.CreateApplyJobOperationDetails; +import com.oracle.bmc.resourcemanager.model.CreateDestroyJobOperationDetails; +import com.oracle.bmc.resourcemanager.model.CreateJobDetails; +import com.oracle.bmc.resourcemanager.model.CreateStackDetails; +import com.oracle.bmc.resourcemanager.model.CreateZipUploadConfigSourceDetails; +import com.oracle.bmc.resourcemanager.model.DestroyJobOperationDetails; +import com.oracle.bmc.resourcemanager.model.Job; +import com.oracle.bmc.resourcemanager.model.Stack; +import com.oracle.bmc.resourcemanager.model.ApplyJobOperationDetails.ExecutionPlanStrategy; +import com.oracle.bmc.resourcemanager.model.Job.Operation; +import com.oracle.bmc.resourcemanager.requests.CreateJobRequest; +import com.oracle.bmc.resourcemanager.requests.CreateStackRequest; +import com.oracle.bmc.resourcemanager.requests.GetJobRequest; +import com.oracle.bmc.resourcemanager.requests.GetStackTfStateRequest; +import com.oracle.bmc.resourcemanager.responses.CreateJobResponse; +import com.oracle.bmc.resourcemanager.responses.CreateStackResponse; +import com.oracle.bmc.resourcemanager.responses.GetJobResponse; +import com.oracle.bmc.resourcemanager.responses.GetStackTfStateResponse; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.stream.JsonParser; + +public class TestRunner { + + // OCI configuration + private static final String TENANCY_SECRET = "OCI_TENANCY_OCID"; + private static final String COMPARTMENT_SECRET = "OCI_COMPARTMENT_OCID"; + private static final String USER_SECRET = "OCI_USER_OCID"; + private static final String FINGERPRINT_SECRET = "OCI_FINGERPRINT"; + private static final String PRIVATE_KEY_SECRET = "OCI_PRIVATE_KEY_PEM"; + + // Stack configuration + private final String zipFileBase64Encoded; + + // OCI SDK + private final AuthenticationDetailsProvider provider; + private final ResourceManagerClient client; + + public TestRunner(String zipFileBase64Encoded) { + this.zipFileBase64Encoded = zipFileBase64Encoded; + + String tenancy_ocid = System.getenv(TENANCY_SECRET); + String user_ocid = System.getenv(USER_SECRET); + String private_key = System.getenv(PRIVATE_KEY_SECRET); + String fingerprint = System.getenv(FINGERPRINT_SECRET); + + provider = SimpleAuthenticationDetailsProvider.builder() + .tenantId(tenancy_ocid) + .userId(user_ocid) + .fingerprint(fingerprint) + .privateKeySupplier(new StringPrivateKeySupplier(private_key)) + .build(); + + client = ResourceManagerClient.builder().region(Region.US_PHOENIX_1).build(provider); + + } + + public String runTestSuite(String testFile) { + try { + String status = "FAILED"; + TestInputList testInputList = TestInputList.fromJsonFile(testFile); + for (TestInput testInput : testInputList.getTestInputList()) { + status = run(testInput); + if (status == "FAILED") { + break; + } + } + return status; + } catch (IOException ex) { + ex.printStackTrace(); + return "FAILED"; + } + } + + public String run(TestInput testInput) { + + System.out.println("Running : " + testInput.getTestName()); + Stack stack = createStack(testInput.getTestName(), testInput.getVariables()); + CreateJobResponse createJobResponse = createApplyJob(stack.getId()); + String status = waitForJobCompleted(createJobResponse); + System.out.println("Create Stack:" + status); + Map terraformState = null; + if (status == "SUCCEDED") { + try { + terraformState = getTerraformState(stack.getId()); + String url = terraformState.get("url"); + System.out.println(url); + for (String path : testInput.getTestUrls()) { + status = checkUrl(url + path); + System.out.println("Check url(" + url + path + "): " + status); + if (status == "FAILED") { + break; + } + } + } catch (Exception ex) { + ex.printStackTrace(); + status = "FAILED"; + } + + if (terraformState != null) { + // delete artifact + String artifact_registry_id = terraformState.get("application_repository_id"); + System.out.println(artifact_registry_id); + String compartment_id = terraformState.get("compartment_id"); + System.out.println(compartment_id); + status = deleteArtifact(artifact_registry_id, compartment_id); + System.out.println("Delete Artifact:" + status); + // destroy stack + CreateJobResponse destroyJobResponse = createDestroyJob(stack.getId()); + status = waitForJobCompleted(destroyJobResponse); + System.out.println(status); + System.out.println("Delete Stack:" + status); + } + + } + + return status; + } + + public String deleteArtifact(String artifact_registry_id, String compartment_id) { + + ArtifactsClient client = ArtifactsClient.builder().region(Region.US_PHOENIX_1).build(provider); + ListGenericArtifactsRequest listGenericArtifactsRequest = ListGenericArtifactsRequest.builder() + .compartmentId(compartment_id) + .repositoryId(artifact_registry_id) + .build(); + + ListGenericArtifactsResponse response = client.listGenericArtifacts(listGenericArtifactsRequest); + for (GenericArtifactSummary item : response.getGenericArtifactCollection().getItems()) { + DeleteGenericArtifactRequest deleteGenericArtifactRequest = DeleteGenericArtifactRequest.builder() + .artifactId(item.getId()) + .opcRequestId(UUID.randomUUID().toString()).build(); + DeleteGenericArtifactResponse deleteResponse = client.deleteGenericArtifact(deleteGenericArtifactRequest); + int statusCode = deleteResponse.get__httpStatusCode__(); + if (statusCode < 200 || statusCode > 300) { + return "FAILED"; + } + } + return "SUCCEDED"; + } + + public Map getTerraformState(String stackId) throws IOException { + Map values = new HashMap<>(); + try { + GetStackTfStateRequest getStackTfStateRequest = GetStackTfStateRequest.builder() + .stackId(stackId) + .opcRequestId(UUID.randomUUID().toString()).build(); + + /* Send request to the Client */ + GetStackTfStateResponse response = client.getStackTfState(getStackTfStateRequest); + + // Parse JSON + JsonParser jsonParser = Json.createParser(response.getInputStream()); + + while (jsonParser.hasNext()) { + JsonParser.Event next = jsonParser.next(); + if (next == JsonParser.Event.START_OBJECT) { + JsonObject object = jsonParser.getObject(); + if (object.containsKey("outputs")) { + String url = object.getJsonObject("outputs").getJsonObject("app_url").getString("value"); + values.put("url", url); + System.out.println("Got url"); + } + if (object.containsKey("resources")) { + JsonArray resources = object.getJsonArray("resources"); + for (int i = 0; i < resources.size(); i++) { + JsonObject resourceObject = resources.get(i).asJsonObject(); + if (resourceObject.getString("name").equals("application_repository")) { + String id = resourceObject.getJsonArray("instances").get(0).asJsonObject().getJsonObject("attributes") + .getString("id"); + values.put("application_repository_id", id); + System.out.println("Got application repository OCID"); + + String compartment_id = resourceObject.getJsonArray("instances").get(0).asJsonObject() + .getJsonObject("attributes") + .getString("compartment_id"); + values.put("compartment_id", compartment_id); + System.out.println("Got compartment OCID"); + break; + } + } + } + } + } + return values; + } catch (Exception ex) { + ex.printStackTrace(); + throw ex; + } + } + + public String checkUrl(String urlString) { + try { + URL url = new URL(urlString); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + int statusCode = connection.getResponseCode(); + if (statusCode < 200 || statusCode > 300) { + System.out.println("Status code: " + statusCode); + return "FAILED"; + } + return "SUCCEDED"; + } catch (Exception ex) { + ex.printStackTrace(); + return "FAILED"; + } + } + + private String waitForJobCompleted(CreateJobResponse createJobResponse) { + + if (createJobResponse != null && createJobResponse.getJob() != null) { + Job.LifecycleState state = createJobResponse.getJob().getLifecycleState(); + System.out.println("Job state : " + state.toString()); + + while (state != Job.LifecycleState.Succeeded && state != Job.LifecycleState.Failed + && state != Job.LifecycleState.Canceled) { + try { + TimeUnit.MINUTES.sleep(3); + state = getJobStatus(createJobResponse.getJob().getId(), createJobResponse.getOpcRequestId()); + } catch (InterruptedException e) { + System.out.println("Sleep error"); + } + + System.out.println("Job state : " + state.toString()); + } + + return state == Job.LifecycleState.Succeeded ? "SUCCEDED" : "FAILED"; + } else { + return "FAILED"; + } + } + + private CreateJobResponse createApplyJob(String stackId) { + CreateJobDetails createJobDetails = CreateJobDetails.builder() + .stackId(stackId) + .displayName("app-stack-test-apply-job-" + UUID.randomUUID().toString()) + .operation(Operation.Apply) + .jobOperationDetails(CreateApplyJobOperationDetails.builder() + .executionPlanStrategy(ExecutionPlanStrategy.AutoApproved) + .build()) + .build(); + + CreateJobRequest createJobRequest = CreateJobRequest.builder() + .createJobDetails(createJobDetails) + .opcRequestId("app-stack-test-apply-job-request-" + UUID.randomUUID() + .toString()) + .opcRetryToken("app-stack-test-apply-job-retry-" + UUID.randomUUID().toString()) + .build(); + + /* Send request to the Client */ + return client.createJob(createJobRequest); + + } + + private CreateJobResponse createDestroyJob(String stackId) { + CreateJobDetails createJobDetails = CreateJobDetails.builder() + .stackId(stackId) + .displayName("app-stack-test-destroy-job-" + UUID.randomUUID().toString()) + .operation(Operation.Destroy) + .jobOperationDetails(CreateDestroyJobOperationDetails.builder() + .executionPlanStrategy(DestroyJobOperationDetails.ExecutionPlanStrategy.AutoApproved) + .build()) + .build(); + + CreateJobRequest createJobRequest = CreateJobRequest.builder() + .createJobDetails(createJobDetails) + .opcRequestId("app-stack-test-destroy-job-request-" + UUID.randomUUID() + .toString()) + .opcRetryToken("app-stack-test-destroy-job-retry-" + UUID.randomUUID().toString()) + .build(); + + /* Send request to the Client */ + return client.createJob(createJobRequest); + + } + + private Job.LifecycleState getJobStatus(String jobId, String opcRequestId) { + GetJobRequest getJobRequest = GetJobRequest.builder() + .jobId(jobId) + .opcRequestId(opcRequestId).build(); + + GetJobResponse response = client.getJob(getJobRequest); + return response.getJob() == null ? Job.LifecycleState.Failed : response.getJob().getLifecycleState(); + + } + + private Stack createStack(String name, Map variables) { + + String compartment_id = System.getenv(COMPARTMENT_SECRET); + + CreateStackDetails createStackDetails = CreateStackDetails.builder() + .compartmentId(compartment_id) + .displayName(LocalDateTime.now().toString() + name) + .description(name) + .configSource(CreateZipUploadConfigSourceDetails.builder() + .zipFileBase64Encoded(zipFileBase64Encoded).build()) + .variables(variables) + .build(); + + CreateStackRequest createStackRequest = CreateStackRequest.builder() + .createStackDetails(createStackDetails) + .opcRequestId("app-stack-test-create-stack-request-" + + UUID.randomUUID() + .toString()) + .opcRetryToken("app-stack-test-create-stack-retry-token-" + UUID.randomUUID().toString()) + .build(); + + /* Send request to the Client */ + CreateStackResponse response = client.createStack(createStackRequest); + return response.getStack(); + + } + +} diff --git a/dotnet/Dockerfile-dotnet.template b/dotnet/Dockerfile-dotnet.template new file mode 100644 index 0000000..d483a5f --- /dev/null +++ b/dotnet/Dockerfile-dotnet.template @@ -0,0 +1,61 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +# dockerfile for running the .NET application using Oracle Linux 8 image +FROM container-registry.oracle.com/os/oraclelinux:8 + +# install asp.net core runtime and its dependencies +RUN dnf install -y aspnetcore-runtime-6.0 && \ + rm -rf /var/cache/dnf + +# create a user so to avoid deploying the application in root directory +RUN useradd -U -d /home/appuser appuser && \ + mkdir /opt/dotnetapp && \ + mkdir /opt/dotnetapp/apm && \ + chown appuser:appuser /opt/dotnetapp /opt/dotnetapp/apm + +# switch the user and create a working directory +USER appuser +WORKDIR /opt/dotnetapp + +# copy application, certificate and wallet folder to working directory +COPY --chown=appuser:appuser servercert.pfx /https/servercert.pfx +COPY --chown=appuser:appuser /dotnetapp . +COPY --chown=appuser:appuser wallet ./wallet + +# set environment variables for running the application on HTTPS port +ENV DOTNET_RUNNING_IN_CONTAINER=true +ENV ASPNETCORE_URLS="https://+:${exposed_port}" +ENV ASPNETCORE_Kestrel__Certificates__Default__Password=${keystore_password} +ENV ASPNETCORE_Kestrel__Certificates__Default__Path=/https/servercert.pfx + +# copy apm installer files to working directory +COPY --chown=appuser:appuser /apm ./apm + +# set environment variables for apm +ENV COR_ENABLE_PROFILING=1 +ENV COR_PROFILER="{918728DD-259F-4A6A-AC2B-B85E1B658318}" +ENV COR_PROFILER_PATH_64=/opt/dotnetapp/apm/tracer-home/win-x64/OpenTelemetry.AutoInstrumentation.Native.dll +ENV COR_PROFILER_PATH_32=/opt/dotnetapp/apm/tracer-home/win-x86/OpenTelemetry.AutoInstrumentation.Native.dll +ENV CORECLR_ENABLE_PROFILING=1 +ENV CORECLR_PROFILER="{918728DD-259F-4A6A-AC2B-B85E1B658318}" +ENV CORECLR_PROFILER_PATH_64=/opt/dotnetapp/apm/tracer-home/win-x64/OpenTelemetry.AutoInstrumentation.Native.dll +ENV CORECLR_PROFILER_PATH_32=/opt/dotnetapp/apm/tracer-home/win-x86/OpenTelemetry.AutoInstrumentation.Native.dll +ENV DOTNET_ADDITIONAL_DEPS=/opt/dotnetapp/apm/tracer-home/AdditionalDeps +ENV DOTNET_SHARED_STORE=/opt/dotnetapp/apm/tracer-home/store +ENV DOTNET_STARTUP_HOOKS=/opt/dotnetapp/apm/tracer-home/net/OpenTelemetry.AutoInstrumentation.StartupHook.dll +ENV OTEL_DOTNET_AUTO_HOME=/apm/tracer-home +ENV OTEL_DOTNET_AUTO_INTEGRATIONS_FILE=/opt/dotnetapp/apm/tracer-home/integrations.json +ENV OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES="OpenTelemetry.ODP" +ENV OTEL_SERVICE_NAME="${application_name}" +ENV OTEL_LOGS_EXPORTER="none" +ENV OTEL_DOTNET_AUTO_EXCLUDE_PROCESSES="dotnet.exe,dotnet" +ENV OTEL_EXPORTER_OTLP_ENDPOINT="${endpoint}/20200101/opentelemetry" +ENV OTEL_EXPORTER_OTLP_HEADERS="Authorization=dataKey ${private_data_key}" +ENV ENABLE_BACKGROUND_ODP=true +ENV ENABLE_CONNECTION_ODP=true + +EXPOSE ${exposed_port} + +# set the entrypoint of the container to run the application +ENTRYPOINT ["dotnet", "${dll_name}" ${program_arguments}] \ No newline at end of file diff --git a/dotnet/build-artifact.yaml.template b/dotnet/build-artifact.yaml.template new file mode 100644 index 0000000..c7c62ca --- /dev/null +++ b/dotnet/build-artifact.yaml.template @@ -0,0 +1,71 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +# This workflow will build and push a .Net application to OCI based on an artifact +version: 0.1 +component: build +timeoutInSeconds: 10000 +shell: bash +env: + vaultVariables: + OCI_TOKEN : "${oci_token}" + DB_USER_PASSWORD : "${db_user_password}" + WALLET_PASSWORD : "${wallet_password}" +inputArtifacts: + - name: dotnetapp + type: GENERIC_ARTIFACT + artifactId: $${artifactId} + registryId: ${registryId} + path: ${artifact_path} + version: $${artifact_version} + location: $${OCI_WORKSPACE_DIR}/${config_repo_name}/${fileName} +steps: + - type: Command + name: Unzip wallet + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${config_repo_name} + unzip wallet.zip -d wallet + - type: Command + name: Download oraclepki and add username and password to wallet + timeoutInSeconds: 300 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${config_repo_name} + curl -o oraclepki.jar "https://repo1.maven.org/maven2/com/oracle/database/security/oraclepki/23.2.0.0/oraclepki-23.2.0.0.jar" -L + echo -e '#/bin/sh\njava -cp oraclepki.jar oracle.security.pki.OracleSecretStoreTextUI -wrl wallet -createCredential "${db_connection_url}" "${db_username}" "'$${DB_USER_PASSWORD}'" <> add-credential-wallet.sh + sh add-credential-wallet.sh + - type: Command + name: Unzip dotnet app + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${config_repo_name} + unzip ${fileName} + cp -r ${artifact_location} dotnetapp + - type: Command + name: Get dotnet apm agent + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${config_repo_name} + oci os object get --namespace idhph4hmky92 --bucket-name prod-agent-binaries --file apm-dotnet-agent-installer-0.6.0.136.zip --name com/oracle/apm/agent/dotnet/apm-dotnet-agent-installer/0.6.0.136/apm-dotnet-agent-installer-0.6.0.136.zip + unzip apm-dotnet-agent-installer-0.6.0.136.zip -d apm + - type: Command + name: Build Docker image + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + docker build . --file Dockerfile --tag ${image_remote_tag}:${image_tag}-$${artifact_version} --tag ${image_latest_tag} + - type: Command + name: Login to repo + timeoutInSeconds: 900 + failImmediatelyOnError: true + command: | + echo $${OCI_TOKEN} | docker login ${container_registry_repo} --username ${login} --password-stdin + - type: Command + name: Push image + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + docker push ${image_remote_tag} --all-tags diff --git a/dotnet/build-repo.yaml.template b/dotnet/build-repo.yaml.template new file mode 100644 index 0000000..4370b58 --- /dev/null +++ b/dotnet/build-repo.yaml.template @@ -0,0 +1,92 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +# This workflow will build and push a .Net application to OCI when a commit is +# pushed to your default branch. +version: 0.1 +component: build +timeoutInSeconds: 3600 +shell: bash +env: + variables: + JAVA_HOME : "/usr/java/latest" + vaultVariables: + OCI_TOKEN : "${oci_token}" + DB_USER_PASSWORD : "${db_user_password}" + WALLET_PASSWORD : "${wallet_password}" +steps: + - type: Command + name: Install DotNet SDK + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm + yum install -y dotnet-sdk-6.0 + onFailure: + - type: Command + command: | + echo $JAVA_HOME + timeoutInSeconds: 400 + - type: Command + name: Build application + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${repo_name} + ${build_command} + onFailure: + - type: Command + command: | + pwd + timeoutInSeconds: 400 + - type: Command + name: Create config files + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${config_repo_name} + unzip wallet.zip -d wallet + - type: Command + name: Download oraclepki and add username and password to wallet + timeoutInSeconds: 300 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${config_repo_name} + curl -o oraclepki.jar "https://repo1.maven.org/maven2/com/oracle/database/security/oraclepki/23.2.0.0/oraclepki-23.2.0.0.jar" -L + echo -e '#/bin/sh\njava -cp oraclepki.jar oracle.security.pki.OracleSecretStoreTextUI -wrl wallet -createCredential "${db_connection_url}" "${db_username}" "'$${DB_USER_PASSWORD}'" <> add-credential-wallet.sh + sh add-credential-wallet.sh + - type: Command + name: Copy DotNet App + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${config_repo_name} + cp -r $${OCI_WORKSPACE_DIR}/${repo_name}/${artifact_location} dotnetapp + - type: Command + name: Get dotnet apm agent + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${config_repo_name} + oci os object get --namespace idhph4hmky92 --bucket-name prod-agent-binaries --file apm-dotnet-agent-installer-0.6.0.136.zip --name com/oracle/apm/agent/dotnet/apm-dotnet-agent-installer/0.6.0.136/apm-dotnet-agent-installer-0.6.0.136.zip + unzip apm-dotnet-agent-installer-0.6.0.136.zip -d apm + - type: Command + name: Build Docker image + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + cd $${OCI_WORKSPACE_DIR}/${repo_name} + export commit=$(git rev-list --all --max-count=1 --abbrev-commit) + cd $${OCI_WORKSPACE_DIR}/${config_repo_name} + docker build . --file Dockerfile --tag ${image_remote_tag}:${image_tag}-$commit --tag ${image_latest_tag} + - type: Command + name: Login to Container Registry + timeoutInSeconds: 900 + failImmediatelyOnError: true + command: | + echo $${OCI_TOKEN} | docker login ${container_registry_repo} --username ${login} --password-stdin + - type: Command + name: Push image + timeoutInSeconds: 600 + failImmediatelyOnError: true + command: | + docker push ${image_remote_tag} --all-tags \ No newline at end of file diff --git a/dotnet/dotnet-config-repo.tf b/dotnet/dotnet-config-repo.tf new file mode 100644 index 0000000..6516c7e --- /dev/null +++ b/dotnet/dotnet-config-repo.tf @@ -0,0 +1,24 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +resource "null_resource" "language_specific_files" { + + depends_on = [ + null_resource.create_config_repo + ] + + # copy certificate + provisioner "local-exec" { + command = "cp server.p12 ./${local.config_repo_name}/servercert.pfx" + on_failure = fail + working_dir = "${path.module}" + } + + # add certificate to git + provisioner "local-exec" { + command = "git add ./servercert.pfx" + on_failure = fail + working_dir = "${path.module}/${local.config_repo_name}" + } + count = (local.use-image ? 0 : 1) +} \ No newline at end of file diff --git a/dotnet/dotnet-datasources.tf b/dotnet/dotnet-datasources.tf new file mode 100644 index 0000000..b62aea6 --- /dev/null +++ b/dotnet/dotnet-datasources.tf @@ -0,0 +1,49 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +# dockerfile used to create image +data "template_file" "dockerfile" { + template = file("${path.module}/Dockerfile-dotnet.template") + vars = { + namespace = "${local.namespace}" + bucket_name = "${local.bucket_name}" + keystore_password = random_password.keystore_password.result + application_name = var.application_name + private_data_key = data.oci_apm_data_keys.private_key.data_keys[0].value + endpoint = oci_apm_apm_domain.app_apm_domain.data_upload_endpoint + program_arguments = (var.program_arguments != null && var.program_arguments != "" ? format(", \"%s\" ", replace(trimspace(var.program_arguments), " ", "\", \"")): "") + exposed_port = var.exposed_port + dll_name = local.dll_name + } +} + +# build spec file +data "template_file" "oci_build_config" { + depends_on = [ + oci_vault_secret.auth_token_secret + ] + template = "${(local.use-repository ? file("${path.module}/build-repo.yaml.template") : file("${path.module}/build-artifact.yaml.template"))}" + vars = { + image_remote_tag = "${local.image-remote-tag}" + image_latest_tag = "${local.image-latest-tag}" + image_tag = "${local.image-name}" + container_registry_repo = "${local.container-registry-repo}" + login = local.login_container + build_command = var.build_command + artifact_location = local.output_path + artifact_path = (local.use-artifact ? data.oci_artifacts_generic_artifact.app_artifact[0].artifact_path : "") + artifact_version = (local.use-artifact ? data.oci_artifacts_generic_artifact.app_artifact[0].version : "") + oci_token = local.auth_token_secret + repo_name = (local.use-repository ? data.oci_devops_repository.devops_repository[0].name : "") + config_repo_name = local.config_repo_name + artifactId = (local.use-artifact ? var.artifact_id : "") + registryId = (local.use-artifact ? var.registry_id : "") + fileName = "app.zip" + db_username = local.username + db_connection_url = local.escaped_connection_url + db_user_password = oci_vault_secret.db_user_password.id + wallet_password = oci_vault_secret.db_wallet_password.id + } +} + + diff --git a/dotnet/dotnet-variables.tf b/dotnet/dotnet-variables.tf new file mode 100644 index 0000000..06cce5b --- /dev/null +++ b/dotnet/dotnet-variables.tf @@ -0,0 +1,19 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +## .NET specific variables and locals +locals { + # Get output folder path and dll name + output_path = "${dirname(var.artifact_location)}/" + dll_name = basename(var.artifact_location) + # path to the wallet + wallet_path = "/opt/dotnetapp/wallet" + + driver_connection_url = ( + var.use_existing_database + ? "${replace(data.oci_database_autonomous_database.autonomous_database.connection_strings[0].profiles[local.conn_url_index].value, "description= ", "description=")}" + : "${replace(oci_database_autonomous_database.database[0].connection_strings[0].profiles[local.conn_url_index].value, "description= ", "description=")}" + ) + # Connection URL environment variable + connection_url_env = "ENV ${var.connection_url_env}=${local.escaped_connection_url}" +} \ No newline at end of file diff --git a/dotnet/interface-app-config-group.yaml b/dotnet/interface-app-config-group.yaml new file mode 100644 index 0000000..54e125a --- /dev/null +++ b/dotnet/interface-app-config-group.yaml @@ -0,0 +1,20 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + + - use_connection_url_env + - connection_url_env + - use_tns_admin_env + - tns_admin_env + - use_username_env + - username_env + - use_password_env + - password_env + - title: "Other parameters" + variables: + - other_environment_variables + - program_arguments + - title: "Application configuration - SSL communication between backends and load balancer" + variables: + - cert_pem + - private_key_pem + - ca_pem diff --git a/dotnet/interface-app-config.yaml b/dotnet/interface-app-config.yaml new file mode 100644 index 0000000..7e6bde2 --- /dev/null +++ b/dotnet/interface-app-config.yaml @@ -0,0 +1,111 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + + # Application configuration + use_connection_url_env: + type: boolean + title: Set connection URL environment variable + default: true + description: Assuming that your application can consume an environment variable to configure the URL, this field can be used to specify the name of the environment variable. + connection_url_env: + type: string + title: Connection URL environment variable name + description: Specify the name of the environment variable. Its value will be set automatically by the stack. + required: true + default: "DATASOURCE_URL" + visible: use_connection_url_env + use_username_env: + type: boolean + title: Set username environment variable + description: Assuming that your application can consume an environment variable to configure the database username, this field can be used to specify the name of the environment variable. + default: false + visible: + eq: + - application_source + - "IMAGE" + username_env: + type: string + title: Database user environment variable name + description: Only the name of the environment variable is needed. The value will be automatically set. If a new database is created, the database ADMIN user will be used. + required: true + default: "DATASOURCE_USERNAME" + visible: use_username_env + use_password_env: + type: boolean + title: Set password environment variable + description: Assuming that your application can consume an environment variable to configure the database user's password, this field can be used to specify the name of the environment variable. + default: false + visible: + eq: + - application_source + - "IMAGE" + password_env: + type: string + title: Database user's password environment variable name + description: Specify the name of the environment variable. Its value will be set automatically by the stack. If a new database is created, the database ADMIN user will be used. + required: true + default: "DATASOURCE_PASSWORD" + visible: use_password_env + use_tns_admin_env: + type: boolean + title: Set TNS_ADMIN environment variable + description: Assuming that your application can consume an environment variable to configure TNS_ADMIN, this field can be used to specify the name of the environment variable. + default: true + visible: + eq: + - application_source + - "IMAGE" + tns_admin_env: + type: string + title: TNS_ADMIN environment variable name + description: Specify the name of the environment variable (Ex. TNS_ADMIN). + required: true + default: "TNS_ADMIN" + visible: + and: + - use_tns_admin_env + - eq: + - application_source + - "IMAGE" + # SSL properties + cert_pem: + type: text + multiline: true + title: SSL certificate + required: true + visible: + eq: + - application_source + - "IMAGE" + private_key_pem: + type: text + multiline: true + title: Private key + required: true + visible: + eq: + - application_source + - "IMAGE" + ca_pem: + type: text + multiline: true + title: CA certificate + required: true + visible: + eq: + - application_source + - "IMAGE" + # Other configuration + other_environment_variables: + type: string + title: Other environment variables + description: If your application can be configured through environment variables you can configure them here. Separate variables with semicolon (var1=value1;var2=value2). + program_arguments: + type: string + title: Program arguments + description: These space-separated program arguments are passed to the .Net application process at startup. + visible: + not: + - eq: + - application_source + - "IMAGE" diff --git a/dotnet/interface-application-group.yaml b/dotnet/interface-application-group.yaml new file mode 100644 index 0000000..931aa1e --- /dev/null +++ b/dotnet/interface-application-group.yaml @@ -0,0 +1,17 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + + - title: "Application" + variables: + - application_name + - nb_copies + - application_source + - devops_compartment + - repo_name + - branch + - build_command + - artifact_location + - registry_id + - artifact_id + - image_path + - exposed_port diff --git a/dotnet/interface-application.yaml b/dotnet/interface-application.yaml new file mode 100644 index 0000000..4e0f8f5 --- /dev/null +++ b/dotnet/interface-application.yaml @@ -0,0 +1,106 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + + application_name: + type: string + required: true + title: Application name + description: This name will be used to name other needed resources. + nb_copies: + type: number + required: true + title: Number of deployments + description: This is the number of container instances that will be deployed. + application_source: + type: enum + title: "Application source" + default: "SOURCE_CODE" + description: You can deploy an application that is either a container image, a .NET artifact (zip archive) or from the source code. + required: true + enum: + - IMAGE + - ARTIFACT + - SOURCE_CODE + devops_compartment: + type: oci:identity:compartment:id + required: true + title: DevOps compartment + description: Compartment containing the DevOps project + default: ${compartment_id} + visible: + not: + - eq: + - application_source + - "SOURCE_CODE" + repo_name: + type: string + required: true + title: DevOps repository (OCID) + description: OCID of the repository containing the application source code. + visible: + eq: + - application_source + - "SOURCE_CODE" + branch: + type: string + required: true + title: Branch used for build / deployment + description: Name of the branch to be built, deployed and on which a trigger will be installed for continuous deployment. + default: main + visible: + eq: + - application_source + - "SOURCE_CODE" + build_command: + type: string + required: true + title: Application build command + description: "Example: dotnet publish {project-path}/{project-name}.csproj -c Release" + visible: + eq: + - application_source + - "SOURCE_CODE" + artifact_location: + type: string + required: true + title: Output Path + description: "Output path of publish folder. Example: bin/{BUILD-CONFIGURATION}/{TFM}/publish/{project-name}.dll" + visible: + not: + - eq: + - application_source + - "IMAGE" + artifact_id: + type: string + required: true + title: Artifact OCID + visible: + eq: + - application_source + - "ARTIFACT" + registry_id: + type: string + required: true + title: Artifact repository OCID + visible: + eq: + - application_source + - "ARTIFACT" + image_path: + type: string + required: true + title: Full path to the image in container registry + visible: + eq: + - application_source + - "IMAGE" + exposed_port: + type: string + required: true + title: Exposed port + description: This is the backend port on which the application is listening. + default: 8443 + visible: + eq: + - application_source + - "IMAGE" diff --git a/dotnet/interface-header.yaml b/dotnet/interface-header.yaml new file mode 100644 index 0000000..72cf107 --- /dev/null +++ b/dotnet/interface-header.yaml @@ -0,0 +1,14 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +title: "App Stack for .NET" +description: | + The App Stack for .NET can deploy existing .NET applications to serverless Container Instances behind a load balancer in the Oracle Cloud. It supports multiple options: the source code of the application is in DevOps, the application is uploaded as a .NET artifact (zip archive), or as a container image. +schemaVersion: 1.1.0 +version: "v0.1.4" +informationalText: | + For more information and product documentation please visit the App Stack project page . + +logoUrl: "https://cloudmarketplace.oracle.com/marketplace/content?contentId=58352039" + +locale: "en" \ No newline at end of file diff --git a/test/appstack-test.jar b/test/appstack-test.jar new file mode 100644 index 0000000..0cd5bfd Binary files /dev/null and b/test/appstack-test.jar differ