Skip to content
Merged
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: 5 additions & 1 deletion app/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
See https://github.com/GoogleCloudPlatform/avocano
This application uses three containers images:

* `client`: https://github.com/GoogleCloudPlatform/avocano/tree/main/client
* `server`: https://github.com/GoogleCloudPlatform/avocano/tree/main/server
* `placeholder`: [placeholder/](placeholder/)
21 changes: 21 additions & 0 deletions app/placeholder/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2023 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.


# Execute with "docker run --build-arg PROJECT_ID=$PROJECT_ID ..."
ARG PROJECT_ID=YOURPROJECTID
FROM gcr.io/$PROJECT_ID/firebase

COPY . ./
ENTRYPOINT ./placeholder-deploy.sh
11 changes: 11 additions & 0 deletions app/placeholder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Placeholder

This folder is designed to hold a static page that can be deployed to Firebase as a placeholder until the real website is deployed.

Features:

* replaces the Firebase Hosting "Site Not Found" 404 page with a placeholder
* page set to automatically refresh every 5 seconds
* uses embedded image (like the "Site Not Found" does) to prevent image loading flickering.

This placeholder is built in `placeholder-image.cloudbuild.json`, automatically built and deployed using Cloud Build triggers for use in the Jump Start Solution terraform.
75 changes: 75 additions & 0 deletions app/placeholder/dist/index.html

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions app/placeholder/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"hosting": {
"public": "dist"
}
}
22 changes: 22 additions & 0 deletions app/placeholder/placeholder-deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash
# Copyright 2023 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.

# Script to assist in Dockerfile-based deployments.
# any errors? exit immediately.
set -e

echo "Deploying placeholder to Firebase..."

firebase deploy --project "$PROJECT_ID" --only hosting
38 changes: 38 additions & 0 deletions app/placeholder/placeholder-image.cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2023 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.

# Build placeholder site code into container

steps:
- id: build
name: "gcr.io/cloud-builders/docker"
dir: app/placeholder
args:
[
"build",
"--build-arg",
"PROJECT_ID=$PROJECT_ID",
"-t",
"gcr.io/$PROJECT_ID/$_IMAGE_NAME",
".",
]

images:
- gcr.io/$PROJECT_ID/$_IMAGE_NAME

substitutions:
_IMAGE_NAME: placeholder

options:
dynamic_substitutions: true
5 changes: 3 additions & 2 deletions infra/containers.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# Base images supplied pre-built.

locals {
server_image = "gcr.io/${var.server_image_host}/server:${var.image_version}"
client_image = "gcr.io/${var.client_image_host}/client:${var.image_version}"
server_image = "gcr.io/${var.server_image_host}/server:${var.image_version}"
client_image = "gcr.io/${var.client_image_host}/client:${var.image_version}"
placeholder_image = "gcr.io/hsa-public/avocano-placeholder:latest"
}
26 changes: 26 additions & 0 deletions infra/jobs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,29 @@ resource "google_cloud_run_v2_job" "client" {
google_project_service.enabled
]
}


resource "google_cloud_run_v2_job" "placeholder" {
name = var.random_suffix ? "placeholder-${random_id.suffix.hex}" : "placeholder"
location = var.region

labels = var.labels

template {
template {
service_account = google_service_account.client.email
containers {
image = local.placeholder_image
env {
name = "PROJECT_ID"
value = var.project_id
}

}
}
}

depends_on = [
google_project_service.enabled
]
}
48 changes: 47 additions & 1 deletion infra/postdeployment.tf
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ resource "google_compute_instance" "gce_init" {
name = var.random_suffix ? "head-start-initialize-${random_id.suffix.hex}" : "head-start-initialize"
machine_type = "n1-standard-1"
zone = var.zone
desired_status = "RUNNING"
desired_status = "RUNNING" # https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/pull/75#issuecomment-1547198414

allow_stopping_for_update = true

Expand Down Expand Up @@ -91,3 +91,49 @@ curl ${local.server_url}/api/products/?warmup
shutdown -h now
EOT
}


resource "google_compute_instance" "placeholder_init" {
count = var.init ? 1 : 0

depends_on = [
google_project_service.enabled,
google_cloud_run_v2_job.placeholder,
]

name = var.random_suffix ? "placeholder-initialize-${random_id.suffix.hex}" : "placeholder-initialize"
machine_type = "n1-standard-1"
zone = var.zone

allow_stopping_for_update = true

boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
}
}

network_interface {
network = google_compute_network.gce_init[0].self_link
subnetwork = google_compute_subnetwork.gce_init[0].self_link

access_config {
// Ephemeral public IP
}
}

service_account {
email = google_service_account.compute[0].email
scopes = ["cloud-platform"] # TODO: Restrict??
}

metadata_startup_script = <<EOT
#!/bin/bash

echo "Running placeholder deployment"
gcloud beta run jobs execute ${google_cloud_run_v2_job.placeholder.name} --wait --project ${var.project_id} --region ${var.region}
curl -X PURGE "${local.firebase_url}/"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussion: This cache purge shouldn't be necessary because we don't expect the firebase site URL to be exposed and checked before it's deploy is complete, unlike the final deploy which ends after tf apply completes. Would be great to learn if that's wrong (and if so, could we have an inline comment?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I was testing this out, I had the URL open in a window, with the "Site not configured", and I found this helped clean that out with the placeholder. It shouldn't have to happen, I just found more success keeping it in.


shutdown -h now
EOT
}
26 changes: 14 additions & 12 deletions infra/test/integration/simple_example/simple_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package multiple_buckets

import (
"fmt"
"io"
"net/http"
"strings"
Expand All @@ -35,26 +34,29 @@ func TestSimpleExample(t *testing.T) {
example.DefineApply(func(assert *assert.Assertions) {
example.DefaultApply(assert)

// Use of this module as part of a Jump Start Solution triggers a URL
// request when terraform apply completes. This primes the Firebase Hosting
// CDN with a platform-supplied 404 page.
// This module deploys a 'placeholder' Firebase Hosting release early
// in the process, to prevent a "Site Not Found" displaying when Terraform
// has finished applying, but the deployment is not yet complete.
//
// This extension of apply is meant to emulate that behavior. We confirm
// the 404 behavior here to boost confidence that the frontend test in
// example.DefineVerify proves the 404 page is fixed.
// the placeholder behavior here to boost confidence that the frontend test in
// example.DefineVerify proves the placeholder page is replaced.
//
// If the check for "Site Not Found" is flaky, remove it in favor of
// If the check is flaky, remove it in favor of
// a simpler HTTP request.
//
// https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/issues/64
firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url")
assertErrorResponseContains(assert, firebase_url, http.StatusNotFound, "Site Not Found")
assertResponseContains(assert, firebase_url, "Your application is still deploying")
})

example.DefineVerify(func(assert *assert.Assertions) {
example.DefaultVerify(assert)

projectID := example.GetTFSetupStringOutput("project_id")
firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url")

flagshipProduct := "Sparkly Avocado"
region := "us-central1"
t.Logf("Using Project ID %q", projectID)

Expand All @@ -81,7 +83,7 @@ func TestSimpleExample(t *testing.T) {
// The data is populated by two Cloud Run jobs, so wait for the final job to finish before continuing.
// A job execution is completed if it has a completed time.
isJobFinished := func() (bool, error) {
clientJobExecs := gcloud.Run(t, "beta run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~client", "--project", projectID, "--region", region, "--format", "json"})).Array()
clientJobExecs := gcloud.Run(t, "run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~client", "--project", projectID, "--region", region, "--format", "json"})).Array()

if len(clientJobExecs) == 0 {
t.Log("Cloud Run job been executed. Retrying...")
Expand All @@ -108,12 +110,12 @@ func TestSimpleExample(t *testing.T) {
utils.Poll(t, isJobFinished, 10, time.Second*10)

// The API must return a list that includes our flagship product
assertResponseContains(assert, serviceURL+"/api/products/", "Sparkly Avocado")
assertResponseContains(assert, serviceURL+"/api/products/", flagshipProduct)
}
{
// Check that the Avocano front page is deployed to Firebase Hosting, and serving
firebaseURL := fmt.Sprintf("https://%s.web.app", projectID)
assertResponseContains(assert, firebaseURL, "<title>Avocano</title>")
t.Log("Firebase Hosting should be running at ", firebase_url)
assertResponseContains(assert, firebase_url, "<title>Avocano</title>")
}
})
example.Test()
Expand Down