diff --git a/functions/v2/label-compute-instance/pom.xml b/functions/v2/label-compute-instance/pom.xml new file mode 100644 index 00000000000..cb574889593 --- /dev/null +++ b/functions/v2/label-compute-instance/pom.xml @@ -0,0 +1,121 @@ + + + + + + 4.0.0 + + com.example.cloud.functions + label-compute-instance + + + com.google.cloud.samples + shared-configuration + 1.0.23 + + + + 11 + 11 + UTF-8 + + + + + + com.google.cloud.functions + functions-framework-api + 1.0.4 + provided + + + com.google.cloud + google-cloud-compute + 1.5.0-alpha + + + com.google.code.gson + gson + 2.8.8 + compile + + + com.google.protobuf + protobuf-java-util + 4.0.0-rc-2 + + + + + com.google.truth + truth + 1.1.3 + test + + + com.google.guava + guava-testlib + 31.0-jre + test + + + io.cloudevents + cloudevents-core + 2.2.0 + test + + + + + + + + com.google.cloud.functions + function-maven-plugin + 0.10.0 + + functions.AutoLabelInstance + + + + org.apache.maven.plugins + maven-surefire-plugin + + + 3.0.0-M5 + + + **/*Test.java + + ${skipTests} + sponge_log + false + + + + + \ No newline at end of file diff --git a/functions/v2/label-compute-instance/src/main/java/functions/AutoLabelInstance.java b/functions/v2/label-compute-instance/src/main/java/functions/AutoLabelInstance.java new file mode 100644 index 00000000000..6fe51bc95dc --- /dev/null +++ b/functions/v2/label-compute-instance/src/main/java/functions/AutoLabelInstance.java @@ -0,0 +1,117 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package functions; + +import com.google.cloud.compute.v1.GetInstanceRequest; +import com.google.cloud.compute.v1.Instance; +import com.google.cloud.compute.v1.InstancesClient; +import com.google.cloud.compute.v1.InstancesSetLabelsRequest; +import com.google.cloud.compute.v1.SetLabelsInstanceRequest; +import com.google.cloud.functions.CloudEventsFunction; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import io.cloudevents.CloudEvent; +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; + +public class AutoLabelInstance implements CloudEventsFunction { + private static final Logger logger = Logger.getLogger(AutoLabelInstance.class.getName()); + + @Override + public void accept(CloudEvent event) throws Exception { + // Extract CloudEvent data + if (event.getData() != null) { + String cloudEventData = new String(event.getData().toBytes(), StandardCharsets.UTF_8); + + // Convert data to JSON + JsonObject eventData; + try { + Gson gson = new Gson(); + eventData = gson.fromJson(cloudEventData, JsonObject.class); + } catch (JsonSyntaxException error) { + throw new RuntimeException("CloudEvent data is not valid JSON: " + error.getMessage()); + } + + // Extract the Cloud Audit Logging entry from the data's protoPayload + JsonObject payload = eventData.getAsJsonObject("protoPayload"); + JsonObject auth = payload.getAsJsonObject("authenticationInfo"); + + // Extract the email address of the authenticated user + // (or service account on behalf of third party principal) making the request + String creator = auth.get("principalEmail").getAsString(); + if (creator == null) { + throw new RuntimeException("`principalEmail` not found in protoPayload."); + } + // Format the 'creator' parameter to match GCE label validation requirements + creator = creator.toLowerCase().replaceAll("\\W", "-"); + + // Get relevant VM instance details from the CloudEvent `subject` property + // Example: compute.googleapis.com/projects//zones//instances/ + String subject = event.getSubject(); + if (subject == null || subject == "") { + throw new RuntimeException("Missing CloudEvent `subject`."); + } + String[] params = subject.split("/"); + + // Validate data + if (params.length < 7) { + throw new RuntimeException("Can not parse resource from CloudEvent `subject`: " + subject); + } + String project = params[2]; + String zone = params[4]; + String instanceName = params[6]; + + // Instantiate the Compute Instances client + try (InstancesClient instancesClient = InstancesClient.create()) { + // Get the newly-created VM instance's label fingerprint + // This is required by the Compute Engine API to prevent duplicate labels + GetInstanceRequest getInstanceRequest = + GetInstanceRequest.newBuilder() + .setInstance(instanceName) + .setProject(project) + .setZone(zone) + .build(); + Instance instance = instancesClient.get(getInstanceRequest); + String fingerPrint = instance.getLabelFingerprint(); + + // Label the instance with its creator + SetLabelsInstanceRequest setLabelRequest = + SetLabelsInstanceRequest.newBuilder() + .setInstance(instanceName) + .setProject(project) + .setZone(zone) + .setInstancesSetLabelsRequestResource( + InstancesSetLabelsRequest.newBuilder() + .putLabels("creator", creator) + .setLabelFingerprint(fingerPrint) + .build()) + .build(); + + instancesClient.setLabels(setLabelRequest); + logger.info( + String.format( + "Adding label, \"{'creator': '%s'}\", to instance, \"%s\".", + creator, instanceName)); + } catch (Exception error) { + throw new RuntimeException( + String.format( + "Error trying to label VM instance, %s: %s", instanceName, error.toString())); + } + } + } +} diff --git a/functions/v2/label-compute-instance/src/test/java/functions/AutoLabelInstanceTest.java b/functions/v2/label-compute-instance/src/test/java/functions/AutoLabelInstanceTest.java new file mode 100644 index 00000000000..986e381e67e --- /dev/null +++ b/functions/v2/label-compute-instance/src/test/java/functions/AutoLabelInstanceTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package functions; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.testing.TestLogHandler; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import java.net.URI; +import java.util.logging.Logger; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AutoLabelInstanceTest { + private static final Logger logger = Logger.getLogger(AutoLabelInstance.class.getName()); + private static final TestLogHandler logHandler = new TestLogHandler(); + + private static final String PROJECT_ID = System.getenv("GOOGLE_CLOUD_PROJECT"); + private static final String ZONE = "us-central1-a"; + private static final String INSTANCE = "lite1"; + + @BeforeClass + public static void beforeClass() { + logger.addHandler(logHandler); + } + + @Test + public void functionsAutoLabelInstance() throws Exception { + // Build a CloudEvent Log Entry + JsonObject protoPayload = new JsonObject(); + JsonObject authInfo = new JsonObject(); + String email = "test@gmail.com"; + authInfo.addProperty("principalEmail", email); + + protoPayload.add("authenticationInfo", authInfo); + + String resource = + String.format( + "compute.googleapis.com/projects/%s/zones/%s/instances/%s", PROJECT_ID, ZONE, INSTANCE); + protoPayload.addProperty("resourceName", resource); + protoPayload.addProperty("methodName", "beta.compute.instances.insert"); + + JsonObject encodedData = new JsonObject(); + encodedData.add("protoPayload", protoPayload); + encodedData.addProperty("name", "test name"); + + CloudEvent event = + CloudEventBuilder.v1() + .withId("0") + .withSubject(resource) + .withType("google.cloud.audit.log.v1.written") + .withSource(URI.create("https://example.com")) + .withData(new Gson().toJson(encodedData).getBytes()) + .build(); + + new AutoLabelInstance().accept(event); + + assertThat( + String.format( + "Adding label, \"{'creator': '%s'}\", to instance, \"%s\".", + "test-gmail-com", INSTANCE)) + .isEqualTo(logHandler.getStoredLogRecords().get(0).getMessage()); + } +}