diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b15f649..1226082 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,11 @@ on: required: true default: 'java' type: string + artifact-prefix: + description: Artifact prefix + required: false + default: '' + type: string jobs: build: runs-on: ubuntu-latest @@ -34,10 +39,10 @@ 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 "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 "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 with: - name: appstackfor${{ inputs.type }} + name: ${{ inputs.artifact-prefix }}appstackfor${{ inputs.type }} path: ./appstackfor${{ inputs.type }}.zip diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml new file mode 100644 index 0000000..403b373 --- /dev/null +++ b/.github/workflows/run-test.yml @@ -0,0 +1,49 @@ +name: 'Test appstack' +on: + workflow_dispatch: + inputs: + branch: + description: Branch name + required: true + default: 'main' + type: string + type: + description: Stack type + required: true + default: 'java' + type: string +jobs: + call-workflow-passing-data: + uses: ./.github/workflows/build.yml + with: + branch: ${{github.ref_name}} + type: 'java' + artifact-prefix: ${{github.sha}}_ + run-test: + needs: call-workflow-passing-data + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.branch }} + - name: download-artifact + uses: actions/download-artifact@v2 + with: + name: ${{github.sha}}_appstackfor${{ inputs.type }} + path: ./test + - name: install-java + uses: actions/setup-java@v3 + with: + distribution: 'oracle' + java-version: '17' + - name: run-test + env: + OCI_TENANCY_OCID: ${{ secrets.OCI_TENANCY_OCID }} + OCI_COMPARTMENT_OCID: ${{ secrets.OCI_COMPARTMENT_OCID }} + OCI_USER_OCID: ${{ secrets.OCI_USER_OCID }} + OCI_PRIVATE_KEY_PEM: ${{ secrets.OCI_PRIVATE_KEY_PEM }} + OCI_FINGERPRINT: ${{ secrets.OCI_FINGERPRINT }} + run: | + cd test + java -jar appstack-test.jar appstackfor${{ inputs.type }}.zip input-${{ inputs.type }}.json \ No newline at end of file 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/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