initial commit
Some checks failed
SonarQube Analysis / Build, Test & Analyse (push) Has been cancelled
Build and Publish TechDocs / build-and-publish (push) Has been cancelled

Change-Id: I12a20fc994c2a94df96de9d3393b06bf6687f77a
This commit is contained in:
Scaffolder
2026-04-17 11:20:50 +00:00
commit 4e3fd72697
376 changed files with 53620 additions and 0 deletions

44
src/adservice/Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
# Copyright 2020 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.
# Define a default value so it's not empty if the builder fails to provide it
ARG BUILDPLATFORM=linux/amd64
FROM --platform=$BUILDPLATFORM eclipse-temurin:24.0.2_12-jdk-noble@sha256:dacac8e9a0df0d2bd24e702b4431132875c249930b70555ebd7ca285b5bee684 AS builder
WORKDIR /app
COPY ["build.gradle", "gradlew", "./"]
COPY gradle gradle
RUN chmod +x gradlew
RUN ./gradlew downloadRepos
COPY . .
RUN chmod +x gradlew
RUN ./gradlew installDist
FROM eclipse-temurin:25.0.2_10-jre-alpine@sha256:f10d6259d0798c1e12179b6bf3b63cea0d6843f7b09c9f9c9c422c50e44379ec
# @TODO: https://github.com/GoogleCloudPlatform/microservices-demo/issues/2517
# Download Stackdriver Profiler Java agent
# RUN mkdir -p /opt/cprof && \
# wget -q -O- https://storage.googleapis.com/cloud-profiler/java/latest/profiler_java_agent_alpine.tar.gz \
# | tar xzv -C /opt/cprof && \
# rm -rf profiler_java_agent.tar.gz
WORKDIR /app
COPY --from=builder /app .
EXPOSE 9555
ENTRYPOINT ["/app/build/install/hipstershop/bin/AdService"]

28
src/adservice/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Ad Service
The Ad service provides advertisement based on context keys. If no context keys are provided then it returns random ads.
## Building locally
The Ad service uses gradlew to compile/install/distribute. Gradle wrapper is already part of the source code. To build Ad Service, run:
```
./gradlew installDist
```
It will create executable script src/adservice/build/install/hipstershop/bin/AdService
### Upgrading gradle version
If you need to upgrade the version of gradle then run
```
./gradlew wrapper --gradle-version <new-version>
```
## Building docker image
From `src/adservice/`, run:
```
docker build ./
```

119
src/adservice/build.gradle Normal file
View File

@@ -0,0 +1,119 @@
plugins {
id 'com.google.protobuf' version '0.9.6'
id 'com.github.sherter.google-java-format' version '0.9'
id 'idea'
id 'application'
}
repositories {
mavenCentral()
mavenLocal()
}
description = 'Ad Service'
group = "adservice"
version = "0.1.0-SNAPSHOT"
def grpcVersion = "1.79.0"
def jacksonCoreVersion = "2.21.1"
def jacksonDatabindVersion = "2.21.1"
def protocVersion = "4.34.0"
tasks.withType(JavaCompile) {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
ext {
speed = project.hasProperty('speed') ? project.getProperty('speed') : false
offlineCompile = new File("$buildDir/output/lib")
}
dependencies {
if (speed) {
implementation fileTree(dir: offlineCompile, include: '*.jar')
} else {
implementation "com.google.api.grpc:proto-google-common-protos:2.66.0",
"javax.annotation:javax.annotation-api:1.3.2",
"io.grpc:grpc-protobuf:${grpcVersion}",
"io.grpc:grpc-stub:${grpcVersion}",
"io.grpc:grpc-netty:${grpcVersion}",
"io.grpc:grpc-services:${grpcVersion}",
"io.grpc:grpc-census:${grpcVersion}",
"org.apache.logging.log4j:log4j-core:2.25.3",
"com.google.protobuf:protobuf-java:${protocVersion}"
runtimeOnly "com.fasterxml.jackson.core:jackson-core:${jacksonCoreVersion}",
"com.fasterxml.jackson.core:jackson-databind:${jacksonDatabindVersion}",
"io.netty:netty-tcnative-boringssl-static:2.0.75.Final"
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protocVersion}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
ofSourceSet('main')
}
}
googleJavaFormat {
toolVersion '1.35.0'
}
// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code.
sourceSets {
main {
java {
srcDirs 'hipstershop'
srcDirs 'build/generated/source/proto/main/java/hipstershop'
srcDirs 'build/generated/source/proto/main/grpc/hipstershop'
}
}
}
startScripts.enabled = false
// This to cache dependencies during Docker image building. First build will take time.
// Subsequent build will be incremental.
task downloadRepos(type: Copy) {
from configurations.compileClasspath
into offlineCompile
from configurations.compileClasspath
into offlineCompile
}
task adService(type: CreateStartScripts) {
mainClass.set('hipstershop.AdService')
applicationName = 'AdService'
outputDir = new File(project.buildDir, 'tmp')
classpath = startScripts.classpath
// @TODO: https://github.com/GoogleCloudPlatform/microservices-demo/issues/2517
// defaultJvmOpts =
// ["-agentpath:/opt/cprof/profiler_java_agent.so=-cprof_service=adservice,-cprof_service_version=1.0.0"]
}
task adServiceClient(type: CreateStartScripts) {
mainClass.set('hipstershop.AdServiceClient')
applicationName = 'AdServiceClient'
outputDir = new File(project.buildDir, 'tmp')
classpath = startScripts.classpath
// @TODO: https://github.com/GoogleCloudPlatform/microservices-demo/issues/2517
// defaultJvmOpts =
// ["-agentpath:/opt/cprof/profiler_java_agent.so=-cprof_service=adserviceclient,-cprof_service_version=1.0.0"]
}
applicationDistribution.into('bin') {
from(adService)
from(adServiceClient)
fileMode = 0755
}

23
src/adservice/genproto.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash -eu
#
# Copyright 2018 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.
# [START gke_adservice_genproto]
# protos are needed in adservice folder for compiling during Docker build.
mkdir -p proto && \
cp ../../protos/demo.proto src/main/proto
# [END gke_adservice_genproto]

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
src/adservice/gradlew vendored Normal file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
src/adservice/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1 @@
rootProject.name = 'hipstershop'

View File

@@ -0,0 +1,238 @@
/*
* Copyright 2018, 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 hipstershop;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import hipstershop.Demo.Ad;
import hipstershop.Demo.AdRequest;
import hipstershop.Demo.AdResponse;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.StatusRuntimeException;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import io.grpc.services.*;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public final class AdService {
private static final Logger logger = LogManager.getLogger(AdService.class);
@SuppressWarnings("FieldCanBeLocal")
private static int MAX_ADS_TO_SERVE = 2;
private Server server;
private HealthStatusManager healthMgr;
private static final AdService service = new AdService();
private void start() throws IOException {
int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "9555"));
healthMgr = new HealthStatusManager();
server =
ServerBuilder.forPort(port)
.addService(new AdServiceImpl())
.addService(healthMgr.getHealthService())
.build()
.start();
logger.info("Ad Service started, listening on " + port);
Runtime.getRuntime()
.addShutdownHook(
new Thread(
() -> {
// Use stderr here since the logger may have been reset by its JVM shutdown hook.
System.err.println(
"*** shutting down gRPC ads server since JVM is shutting down");
AdService.this.stop();
System.err.println("*** server shut down");
}));
healthMgr.setStatus("", ServingStatus.SERVING);
}
private void stop() {
if (server != null) {
healthMgr.clearStatus("");
server.shutdown();
}
}
private static class AdServiceImpl extends hipstershop.AdServiceGrpc.AdServiceImplBase {
/**
* Retrieves ads based on context provided in the request {@code AdRequest}.
*
* @param req the request containing context.
* @param responseObserver the stream observer which gets notified with the value of {@code
* AdResponse}
*/
@Override
public void getAds(AdRequest req, StreamObserver<AdResponse> responseObserver) {
AdService service = AdService.getInstance();
try {
List<Ad> allAds = new ArrayList<>();
logger.info("received ad request (context_words=" + req.getContextKeysList() + ")");
if (req.getContextKeysCount() > 0) {
for (int i = 0; i < req.getContextKeysCount(); i++) {
Collection<Ad> ads = service.getAdsByCategory(req.getContextKeys(i));
allAds.addAll(ads);
}
} else {
allAds = service.getRandomAds();
}
if (allAds.isEmpty()) {
// Serve random ads.
allAds = service.getRandomAds();
}
AdResponse reply = AdResponse.newBuilder().addAllAds(allAds).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (StatusRuntimeException e) {
logger.log(Level.WARN, "GetAds Failed with status {}", e.getStatus());
responseObserver.onError(e);
}
}
}
private static final ImmutableListMultimap<String, Ad> adsMap = createAdsMap();
private Collection<Ad> getAdsByCategory(String category) {
return adsMap.get(category);
}
private static final Random random = new Random();
private List<Ad> getRandomAds() {
List<Ad> ads = new ArrayList<>(MAX_ADS_TO_SERVE);
Collection<Ad> allAds = adsMap.values();
for (int i = 0; i < MAX_ADS_TO_SERVE; i++) {
ads.add(Iterables.get(allAds, random.nextInt(allAds.size())));
}
return ads;
}
private static AdService getInstance() {
return service;
}
/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
private static ImmutableListMultimap<String, Ad> createAdsMap() {
Ad hairdryer =
Ad.newBuilder()
.setRedirectUrl("/product/2ZYFJ3GM2N")
.setText("Hairdryer for sale. 50% off.")
.build();
Ad tankTop =
Ad.newBuilder()
.setRedirectUrl("/product/66VCHSJNUP")
.setText("Tank top for sale. 20% off.")
.build();
Ad candleHolder =
Ad.newBuilder()
.setRedirectUrl("/product/0PUK6V6EV0")
.setText("Candle holder for sale. 30% off.")
.build();
Ad bambooGlassJar =
Ad.newBuilder()
.setRedirectUrl("/product/9SIQT8TOJO")
.setText("Bamboo glass jar for sale. 10% off.")
.build();
Ad watch =
Ad.newBuilder()
.setRedirectUrl("/product/1YMWWN1N4O")
.setText("Watch for sale. Buy one, get second kit for free")
.build();
Ad mug =
Ad.newBuilder()
.setRedirectUrl("/product/6E92ZMYYFZ")
.setText("Mug for sale. Buy two, get third one for free")
.build();
Ad loafers =
Ad.newBuilder()
.setRedirectUrl("/product/L9ECAV7KIM")
.setText("Loafers for sale. Buy one, get second one for free")
.build();
return ImmutableListMultimap.<String, Ad>builder()
.putAll("clothing", tankTop)
.putAll("accessories", watch)
.putAll("footwear", loafers)
.putAll("hair", hairdryer)
.putAll("decor", candleHolder)
.putAll("kitchen", bambooGlassJar, mug)
.build();
}
private static void initStats() {
if (System.getenv("DISABLE_STATS") != null) {
logger.info("Stats disabled.");
return;
}
logger.info("Stats enabled, but temporarily unavailable");
long sleepTime = 10; /* seconds */
int maxAttempts = 5;
// TODO(arbrown) Implement OpenTelemetry stats
}
private static void initTracing() {
if (System.getenv("DISABLE_TRACING") != null) {
logger.info("Tracing disabled.");
return;
}
logger.info("Tracing enabled but temporarily unavailable");
logger.info("See https://github.com/GoogleCloudPlatform/microservices-demo/issues/422 for more info.");
// TODO(arbrown) Implement OpenTelemetry tracing
logger.info("Tracing enabled - Stackdriver exporter initialized.");
}
/** Main launches the server from the command line. */
public static void main(String[] args) throws IOException, InterruptedException {
new Thread(
() -> {
initStats();
initTracing();
})
.start();
// Start the RPC server. You shouldn't see any output from gRPC before this.
logger.info("AdService starting.");
final AdService service = AdService.getInstance();
service.start();
service.blockUntilShutdown();
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2018, 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 hipstershop;
import hipstershop.Demo.Ad;
import hipstershop.Demo.AdRequest;
import hipstershop.Demo.AdResponse;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** A simple client that requests ads from the Ads Service. */
public class AdServiceClient {
private static final Logger logger = LogManager.getLogger(AdServiceClient.class);
private final ManagedChannel channel;
private final hipstershop.AdServiceGrpc.AdServiceBlockingStub blockingStub;
/** Construct client connecting to Ad Service at {@code host:port}. */
private AdServiceClient(String host, int port) {
this(
ManagedChannelBuilder.forAddress(host, port)
// Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
// needing certificates.
.usePlaintext()
.build());
}
/** Construct client for accessing RouteGuide server using the existing channel. */
private AdServiceClient(ManagedChannel channel) {
this.channel = channel;
blockingStub = hipstershop.AdServiceGrpc.newBlockingStub(channel);
}
private void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
/** Get Ads from Server. */
public void getAds(String contextKey) {
logger.info("Get Ads with context " + contextKey + " ...");
AdRequest request = AdRequest.newBuilder().addContextKeys(contextKey).build();
AdResponse response;
try {
response = blockingStub.getAds(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARN, "RPC failed: " + e.getStatus());
return;
}
for (Ad ads : response.getAdsList()) {
logger.info("Ads: " + ads.getText());
}
}
private static int getPortOrDefaultFromArgs(String[] args) {
int portNumber = 9555;
if (2 < args.length) {
try {
portNumber = Integer.parseInt(args[2]);
} catch (NumberFormatException e) {
logger.warn(String.format("Port %s is invalid, use default port %d.", args[2], 9555));
}
}
return portNumber;
}
private static String getStringOrDefaultFromArgs(
String[] args, int index, @Nullable String defaultString) {
String s = defaultString;
if (index < args.length) {
s = args[index];
}
return s;
}
/**
* Ads Service Client main. If provided, the first element of {@code args} is the context key to
* get the ads from the Ads Service
*/
public static void main(String[] args) throws InterruptedException {
// Add final keyword to pass checkStyle.
final String contextKeys = getStringOrDefaultFromArgs(args, 0, "camera");
final String host = getStringOrDefaultFromArgs(args, 1, "localhost");
final int serverPort = getPortOrDefaultFromArgs(args);
AdServiceClient client = new AdServiceClient(host, serverPort);
try {
client.getAds(contextKeys);
} finally {
client.shutdown();
}
logger.info("Exiting AdServiceClient...");
}
}

View File

@@ -0,0 +1,260 @@
// Copyright 2020 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.
syntax = "proto3";
package hipstershop;
// -----------------Cart service-----------------
service CartService {
rpc AddItem(AddItemRequest) returns (Empty) {}
rpc GetCart(GetCartRequest) returns (Cart) {}
rpc EmptyCart(EmptyCartRequest) returns (Empty) {}
}
message CartItem {
string product_id = 1;
int32 quantity = 2;
}
message AddItemRequest {
string user_id = 1;
CartItem item = 2;
}
message EmptyCartRequest {
string user_id = 1;
}
message GetCartRequest {
string user_id = 1;
}
message Cart {
string user_id = 1;
repeated CartItem items = 2;
}
message Empty {}
// ---------------Recommendation service----------
service RecommendationService {
rpc ListRecommendations(ListRecommendationsRequest) returns (ListRecommendationsResponse){}
}
message ListRecommendationsRequest {
string user_id = 1;
repeated string product_ids = 2;
}
message ListRecommendationsResponse {
repeated string product_ids = 1;
}
// ---------------Product Catalog----------------
service ProductCatalogService {
rpc ListProducts(Empty) returns (ListProductsResponse) {}
rpc GetProduct(GetProductRequest) returns (Product) {}
rpc SearchProducts(SearchProductsRequest) returns (SearchProductsResponse) {}
}
message Product {
string id = 1;
string name = 2;
string description = 3;
string picture = 4;
Money price_usd = 5;
// Categories such as "clothing" or "kitchen" that can be used to look up
// other related products.
repeated string categories = 6;
}
message ListProductsResponse {
repeated Product products = 1;
}
message GetProductRequest {
string id = 1;
}
message SearchProductsRequest {
string query = 1;
}
message SearchProductsResponse {
repeated Product results = 1;
}
// ---------------Shipping Service----------
service ShippingService {
rpc GetQuote(GetQuoteRequest) returns (GetQuoteResponse) {}
rpc ShipOrder(ShipOrderRequest) returns (ShipOrderResponse) {}
}
message GetQuoteRequest {
Address address = 1;
repeated CartItem items = 2;
}
message GetQuoteResponse {
Money cost_usd = 1;
}
message ShipOrderRequest {
Address address = 1;
repeated CartItem items = 2;
}
message ShipOrderResponse {
string tracking_id = 1;
}
message Address {
string street_address = 1;
string city = 2;
string state = 3;
string country = 4;
int32 zip_code = 5;
}
// -----------------Currency service-----------------
service CurrencyService {
rpc GetSupportedCurrencies(Empty) returns (GetSupportedCurrenciesResponse) {}
rpc Convert(CurrencyConversionRequest) returns (Money) {}
}
// Represents an amount of money with its currency type.
message Money {
// The 3-letter currency code defined in ISO 4217.
string currency_code = 1;
// The whole units of the amount.
// For example if `currencyCode` is `"USD"`, then 1 unit is one US dollar.
int64 units = 2;
// Number of nano (10^-9) units of the amount.
// The value must be between -999,999,999 and +999,999,999 inclusive.
// If `units` is positive, `nanos` must be positive or zero.
// If `units` is zero, `nanos` can be positive, zero, or negative.
// If `units` is negative, `nanos` must be negative or zero.
// For example $-1.75 is represented as `units`=-1 and `nanos`=-750,000,000.
int32 nanos = 3;
}
message GetSupportedCurrenciesResponse {
// The 3-letter currency code defined in ISO 4217.
repeated string currency_codes = 1;
}
message CurrencyConversionRequest {
Money from = 1;
// The 3-letter currency code defined in ISO 4217.
string to_code = 2;
}
// -------------Payment service-----------------
service PaymentService {
rpc Charge(ChargeRequest) returns (ChargeResponse) {}
}
message CreditCardInfo {
string credit_card_number = 1;
int32 credit_card_cvv = 2;
int32 credit_card_expiration_year = 3;
int32 credit_card_expiration_month = 4;
}
message ChargeRequest {
Money amount = 1;
CreditCardInfo credit_card = 2;
}
message ChargeResponse {
string transaction_id = 1;
}
// -------------Email service-----------------
service EmailService {
rpc SendOrderConfirmation(SendOrderConfirmationRequest) returns (Empty) {}
}
message OrderItem {
CartItem item = 1;
Money cost = 2;
}
message OrderResult {
string order_id = 1;
string shipping_tracking_id = 2;
Money shipping_cost = 3;
Address shipping_address = 4;
repeated OrderItem items = 5;
}
message SendOrderConfirmationRequest {
string email = 1;
OrderResult order = 2;
}
// -------------Checkout service-----------------
service CheckoutService {
rpc PlaceOrder(PlaceOrderRequest) returns (PlaceOrderResponse) {}
}
message PlaceOrderRequest {
string user_id = 1;
string user_currency = 2;
Address address = 3;
string email = 5;
CreditCardInfo credit_card = 6;
}
message PlaceOrderResponse {
OrderResult order = 1;
}
// ------------Ad service------------------
service AdService {
rpc GetAds(AdRequest) returns (AdResponse) {}
}
message AdRequest {
// List of important key words from the current page describing the context.
repeated string context_keys = 1;
}
message AdResponse {
repeated Ad ads = 1;
}
message Ad {
// url to redirect to when an ad is clicked.
string redirect_url = 1;
// short advertisement text to display.
string text = 2;
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2020 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.
-->
<Configuration status="WARN">
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<!-- This is a JSON format that can be read by the Stackdriver Logging agent. The trace ID,
span ID, sampling decision, and timestamp are interpreted by Stackdriver. It uses the
special JSON keys that the Stackdriver Logging agent converts to "trace", "spanId",
"traceSampled", and "timestamp" in the Stackdriver LogEntry
(https://cloud.google.com/logging/docs/agent/configuration#special-fields). -->
<JsonLayout compact="true" eventEol="true">
<KeyValuePair key="logging.googleapis.com/trace" value="$${ctx:traceId}"/>
<KeyValuePair key="logging.googleapis.com/spanId" value="$${ctx:spanId}"/>
<KeyValuePair key="logging.googleapis.com/traceSampled" value="$${ctx:traceSampled}"/>
<KeyValuePair key="time" value="$${date:yyyy-MM-dd}T$${date:HH:mm:ss.SSS}Z"/>
</JsonLayout>
</Console>
</Appenders>
<Loggers>
<Logger name="io.grpc.netty" level="INFO"/>
<Logger name="io.netty" level="INFO"/>
<Logger name="sun.net" level="INFO"/>
<Root level="TRACE">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,48 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cartservice", "src\cartservice.csproj", "{2348C29F-E8D3-4955-916D-D609CBC97FCB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cartservice.tests", "tests\cartservice.tests.csproj", "{59825342-CE64-4AFA-8744-781692C0811B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x64.ActiveCfg = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x64.Build.0 = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x86.ActiveCfg = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x86.Build.0 = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|Any CPU.Build.0 = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x64.ActiveCfg = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x64.Build.0 = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x86.ActiveCfg = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x86.Build.0 = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x64.ActiveCfg = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x64.Build.0 = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x86.ActiveCfg = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x86.Build.0 = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|Any CPU.Build.0 = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x64.ActiveCfg = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x64.Build.0 = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x86.ActiveCfg = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,6 @@
**/*.sh
**/*.bat
**/bin/
**/obj/
**/out/
Dockerfile*

View File

@@ -0,0 +1,44 @@
# 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.
# Define a default value so it's not empty if the builder fails to provide it
ARG BUILDPLATFORM=linux/amd64
# https://mcr.microsoft.com/product/dotnet/sdk
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0.100-noble@sha256:c7445f141c04f1a6b454181bd098dcfa606c61ba0bd213d0a702489e5bd4cd71 AS builder
ARG TARGETARCH=amd64
WORKDIR /app
COPY cartservice.csproj .
RUN dotnet restore cartservice.csproj \
-a $TARGETARCH
COPY . .
RUN dotnet publish cartservice.csproj \
-p:PublishSingleFile=true \
-a $TARGETARCH \
--self-contained true \
-p:PublishTrimmed=true \
-p:TrimMode=full \
-c release \
-o /cartservice
# https://mcr.microsoft.com/product/dotnet/runtime-deps
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0.0-noble-chiseled@sha256:b857c8cb8d929183cfe4c6dd9994abba92a2639dd2dbaf06005379f815991604
WORKDIR /app
COPY --from=builder /cartservice .
EXPOSE 7070
ENV DOTNET_EnableDiagnostics=0 \
ASPNETCORE_HTTP_PORTS=7070
USER 1000
ENTRYPOINT ["/app/cartservice"]

View File

@@ -0,0 +1,33 @@
# 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.
FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:478b9038d187e5b5c29bfa8173ded5d29e864b5ad06102a12106380ee01e2e49 AS build
WORKDIR /app
COPY . .
RUN dotnet restore cartservice.csproj
RUN dotnet build "./cartservice.csproj" -c Debug -o /out
FROM build AS publish
RUN dotnet publish cartservice.csproj -c Debug -o /out
# Building final image used in running container
FROM mcr.microsoft.com/dotnet/aspnet:10.0@sha256:a04d1c1d2d26119049494057d80ea6cda25bbd8aef7c444a1fc1ef874fd3955b AS final
# Installing procps on the container to enable debugging of .NET Core
RUN apt-get update \
&& apt-get install -y unzip procps wget
WORKDIR /app
COPY --from=publish /out .
ENV ASPNETCORE_HTTP_PORTS=7070
ENTRYPOINT ["dotnet", "cartservice.dll"]

View File

@@ -0,0 +1,26 @@
// Copyright 2020 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.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using cartservice;
CreateHostBuilder(args).Build().Run();
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});

View File

@@ -0,0 +1,84 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using cartservice.cartstore;
using cartservice.services;
using Microsoft.Extensions.Caching.StackExchangeRedis;
namespace cartservice
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
string redisAddress = Configuration["REDIS_ADDR"];
string spannerProjectId = Configuration["SPANNER_PROJECT"];
string spannerConnectionString = Configuration["SPANNER_CONNECTION_STRING"];
string alloyDBConnectionString = Configuration["ALLOYDB_PRIMARY_IP"];
if (!string.IsNullOrEmpty(redisAddress))
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisAddress;
});
services.AddSingleton<ICartStore, RedisCartStore>();
}
else if (!string.IsNullOrEmpty(spannerProjectId) || !string.IsNullOrEmpty(spannerConnectionString))
{
services.AddSingleton<ICartStore, SpannerCartStore>();
}
else if (!string.IsNullOrEmpty(alloyDBConnectionString))
{
Console.WriteLine("Creating AlloyDB cart store");
services.AddSingleton<ICartStore, AlloyDBCartStore>();
}
else
{
Console.WriteLine("Redis cache host(hostname+port) was not specified. Starting a cart service using in memory store");
services.AddDistributedMemoryCache();
services.AddSingleton<ICartStore, RedisCartStore>();
}
services.AddGrpc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<CartService>();
endpoints.MapGrpcService<cartservice.services.HealthCheckService>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
});
});
}
}
}

View File

@@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
<PackageReference Include="Grpc.HealthCheck" Version="2.76.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.5" />
<PackageReference Include="Google.Cloud.Spanner.Data" Version="5.12.0" />
<PackageReference Include="Npgsql" Version="10.0.2" />
<PackageReference Include="Google.Cloud.SecretManager.V1" Version="2.7.0" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="protos\Cart.proto" GrpcServices="Both" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,177 @@
// 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.
using System;
using Grpc.Core;
using Npgsql;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
using Google.Api.Gax.ResourceNames;
using Google.Cloud.SecretManager.V1;
namespace cartservice.cartstore
{
public class AlloyDBCartStore : ICartStore
{
private readonly string tableName;
private readonly string connectionString;
public AlloyDBCartStore(IConfiguration configuration)
{
// Create a Cloud Secrets client.
SecretManagerServiceClient client = SecretManagerServiceClient.Create();
var projectId = configuration["PROJECT_ID"];
var secretId = configuration["ALLOYDB_SECRET_NAME"];
SecretVersionName secretVersionName = new SecretVersionName(projectId, secretId, "latest");
AccessSecretVersionResponse result = client.AccessSecretVersion(secretVersionName);
// Convert the payload to a string. Payloads are bytes by default.
string alloyDBPassword = result.Payload.Data.ToStringUtf8().TrimEnd('\r', '\n');
// TODO: Create a separate user for connecting within the application
// rather than using our superuser
string alloyDBUser = "postgres";
string databaseName = configuration["ALLOYDB_DATABASE_NAME"];
// TODO: Consider splitting workloads into read vs. write and take
// advantage of the AlloyDB read pools
string primaryIPAddress = configuration["ALLOYDB_PRIMARY_IP"];
connectionString = "Host=" +
primaryIPAddress +
";Username=" +
alloyDBUser +
";Password=" +
alloyDBPassword +
";Database=" +
databaseName;
tableName = configuration["ALLOYDB_TABLE_NAME"];
}
public async Task AddItemAsync(string userId, string productId, int quantity)
{
Console.WriteLine($"AddItemAsync for {userId} called");
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString);
var fetchCmd = $"SELECT quantity FROM {tableName} WHERE userID='{userId}' AND productID='{productId}'";
var currentQuantity = 0;
var cmdRead = dataSource.CreateCommand(fetchCmd);
await using (var reader = await cmdRead.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
currentQuantity += reader.GetInt32(0);
}
var totalQuantity = quantity + currentQuantity;
// Use INSERT ... ON CONFLICT to prevent duplicate key error
var insertCmd = $@"
INSERT INTO {tableName} (userId, productId, quantity)
VALUES ('{userId}', '{productId}', {totalQuantity})
ON CONFLICT (userId, productId)
DO UPDATE SET quantity = {totalQuantity};
";
await using (var cmdInsert = dataSource.CreateCommand(insertCmd))
{
await Task.Run(() =>
{
return cmdInsert.ExecuteNonQueryAsync();
});
}
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Unable to access cart storage due to an internal error. {ex}"));
}
}
public async Task<Hipstershop.Cart> GetCartAsync(string userId)
{
Console.WriteLine($"GetCartAsync called for userId={userId}");
Hipstershop.Cart cart = new();
cart.UserId = userId;
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString);
var cartFetchCmd = $"SELECT productId, quantity FROM {tableName} WHERE userId = '{userId}'";
var cmd = dataSource.CreateCommand(cartFetchCmd);
await using (var reader = await cmd.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
Hipstershop.CartItem item = new()
{
ProductId = reader.GetString(0),
Quantity = reader.GetInt32(1)
};
cart.Items.Add(item);
}
}
await Task.Run(() =>
{
return cart;
});
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Unable to access cart storage due to an internal error. {ex}"));
}
return cart;
}
public async Task EmptyCartAsync(string userId)
{
Console.WriteLine($"EmptyCartAsync called for userId={userId}");
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString);
var deleteCmd = $"DELETE FROM {tableName} WHERE userID = '{userId}'";
await using (var cmd = dataSource.CreateCommand(deleteCmd))
{
await Task.Run(() =>
{
return cmd.ExecuteNonQueryAsync();
});
}
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Unable to access cart storage due to an internal error. {ex}"));
}
}
public bool Ping()
{
try
{
return true;
}
catch (Exception)
{
return false;
}
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2018 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.
using System.Threading.Tasks;
namespace cartservice.cartstore
{
public interface ICartStore
{
Task AddItemAsync(string userId, string productId, int quantity);
Task EmptyCartAsync(string userId);
Task<Hipstershop.Cart> GetCartAsync(string userId);
bool Ping();
}
}

View File

@@ -0,0 +1,118 @@
// Copyright 2018 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.
using System;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.Extensions.Caching.Distributed;
using Google.Protobuf;
namespace cartservice.cartstore
{
public class RedisCartStore : ICartStore
{
private readonly IDistributedCache _cache;
public RedisCartStore(IDistributedCache cache)
{
_cache = cache;
}
public async Task AddItemAsync(string userId, string productId, int quantity)
{
Console.WriteLine($"AddItemAsync called with userId={userId}, productId={productId}, quantity={quantity}");
try
{
Hipstershop.Cart cart;
var value = await _cache.GetAsync(userId);
if (value == null)
{
cart = new Hipstershop.Cart();
cart.UserId = userId;
cart.Items.Add(new Hipstershop.CartItem { ProductId = productId, Quantity = quantity });
}
else
{
cart = Hipstershop.Cart.Parser.ParseFrom(value);
var existingItem = cart.Items.SingleOrDefault(i => i.ProductId == productId);
if (existingItem == null)
{
cart.Items.Add(new Hipstershop.CartItem { ProductId = productId, Quantity = quantity });
}
else
{
existingItem.Quantity += quantity;
}
}
await _cache.SetAsync(userId, cart.ToByteArray());
}
catch (Exception ex)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}"));
}
}
public async Task EmptyCartAsync(string userId)
{
Console.WriteLine($"EmptyCartAsync called with userId={userId}");
try
{
var cart = new Hipstershop.Cart();
await _cache.SetAsync(userId, cart.ToByteArray());
}
catch (Exception ex)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}"));
}
}
public async Task<Hipstershop.Cart> GetCartAsync(string userId)
{
Console.WriteLine($"GetCartAsync called with userId={userId}");
try
{
// Access the cart from the cache
var value = await _cache.GetAsync(userId);
if (value != null)
{
return Hipstershop.Cart.Parser.ParseFrom(value);
}
// We decided to return empty cart in cases when user wasn't in the cache before
return new Hipstershop.Cart();
}
catch (Exception ex)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}"));
}
}
public bool Ping()
{
try
{
return true;
}
catch (Exception)
{
return false;
}
}
}
}

View File

@@ -0,0 +1,185 @@
// 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.
using System;
using Google.Cloud.Spanner.Data;
using Grpc.Core;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
namespace cartservice.cartstore
{
public class SpannerCartStore : ICartStore
{
private static readonly string TableName = "CartItems";
private static readonly string DefaultInstanceName = "onlineboutique";
private static readonly string DefaultDatabaseName = "carts";
private readonly string databaseString;
public SpannerCartStore(IConfiguration configuration)
{
string spannerProjectId = configuration["SPANNER_PROJECT"];
string spannerInstanceId = configuration["SPANNER_INSTANCE"];
string spannerDatabaseId = configuration["SPANNER_DATABASE"];
string spannerConnectionString = configuration["SPANNER_CONNECTION_STRING"];
SpannerConnectionStringBuilder builder = new();
if (!string.IsNullOrEmpty(spannerConnectionString)) {
builder.DataSource = spannerConnectionString;
databaseString = builder.ToString();
Console.WriteLine($"Spanner connection string: ${databaseString}");
return;
}
if (string.IsNullOrEmpty(spannerInstanceId))
spannerInstanceId = DefaultInstanceName;
if (string.IsNullOrEmpty(spannerDatabaseId))
spannerDatabaseId = DefaultDatabaseName;
builder.DataSource =
$"projects/{spannerProjectId}/instances/{spannerInstanceId}/databases/{spannerDatabaseId}";
databaseString = builder.ToString();
Console.WriteLine($"Built Spanner connection string: '{databaseString}'");
}
public async Task AddItemAsync(string userId, string productId, int quantity)
{
Console.WriteLine($"AddItemAsync for {userId} called");
try
{
using SpannerConnection spannerConnection = new(databaseString);
await spannerConnection.RunWithRetriableTransactionAsync(async transaction =>
{
int currentQuantity = 0;
var quantityLookup = spannerConnection.CreateSelectCommand(
$"SELECT * FROM {TableName} WHERE userId = @userId AND productId = @productId",
new SpannerParameterCollection
{
{ "userId", SpannerDbType.String },
{ "productId", SpannerDbType.String }
});
quantityLookup.Parameters["userId"].Value = userId;
quantityLookup.Parameters["productId"].Value = productId;
quantityLookup.Transaction = transaction;
using (var reader = await quantityLookup.ExecuteReaderAsync())
{
while (await reader.ReadAsync()) {
currentQuantity += reader.GetFieldValue<int>("quantity");
}
}
var cmd = spannerConnection.CreateInsertOrUpdateCommand(TableName,
new SpannerParameterCollection
{
{ "userId", SpannerDbType.String },
{ "productId", SpannerDbType.String },
{ "quantity", SpannerDbType.Int64 }
});
cmd.Parameters["userId"].Value = userId;
cmd.Parameters["productId"].Value = productId;
cmd.Parameters["quantity"].Value = currentQuantity + quantity;
cmd.Transaction = transaction;
await Task.Run(() =>
{
return cmd.ExecuteNonQueryAsync();
});
});
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Can't access cart storage at {databaseString}. {ex}"));
}
}
public async Task<Hipstershop.Cart> GetCartAsync(string userId)
{
Console.WriteLine($"GetCartAsync called for userId={userId}");
Hipstershop.Cart cart = new();
try
{
using SpannerConnection spannerConnection = new(databaseString);
var cmd = spannerConnection.CreateSelectCommand(
$"SELECT * FROM {TableName} WHERE userId = @userId",
new SpannerParameterCollection {
{ "userId", SpannerDbType.String }
}
);
cmd.Parameters["userId"].Value = userId;
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
// Only add the userId if something is in the cart.
// This is based on how the cartservice example behaves.
// An empty cart has no userId attached.
cart.UserId = userId;
Hipstershop.CartItem item = new()
{
ProductId = reader.GetFieldValue<string>("productId"),
Quantity = reader.GetFieldValue<int>("quantity")
};
cart.Items.Add(item);
}
return cart;
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Can't access cart storage at {databaseString}. {ex}"));
}
}
public async Task EmptyCartAsync(string userId)
{
Console.WriteLine($"EmptyCartAsync called for userId={userId}");
try
{
using SpannerConnection spannerConnection = new(databaseString);
await Task.Run(() =>
{
var cmd = spannerConnection.CreateDmlCommand(
$"DELETE FROM {TableName} WHERE userId = @userId",
new SpannerParameterCollection
{
{ "userId", SpannerDbType.String }
});
cmd.Parameters["userId"].Value = userId;
return cmd.ExecuteNonQueryAsync();
});
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Can't access cart storage at {databaseString}. {ex}"));
}
}
public bool Ping()
{
try
{
return true;
}
catch (Exception)
{
return false;
}
}
}
}

View File

@@ -0,0 +1,50 @@
// Copyright 2020 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.
syntax = "proto3";
package hipstershop;
// -----------------Cart service-----------------
service CartService {
rpc AddItem(AddItemRequest) returns (Empty) {}
rpc GetCart(GetCartRequest) returns (Cart) {}
rpc EmptyCart(EmptyCartRequest) returns (Empty) {}
}
message CartItem {
string product_id = 1;
int32 quantity = 2;
}
message AddItemRequest {
string user_id = 1;
CartItem item = 2;
}
message EmptyCartRequest {
string user_id = 1;
}
message GetCartRequest {
string user_id = 1;
}
message Cart {
string user_id = 1;
repeated CartItem items = 2;
}
message Empty {}

View File

@@ -0,0 +1,51 @@
// Copyright 2020 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.
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using cartservice.cartstore;
using Hipstershop;
namespace cartservice.services
{
public class CartService : Hipstershop.CartService.CartServiceBase
{
private readonly static Empty Empty = new Empty();
private readonly ICartStore _cartStore;
public CartService(ICartStore cartStore)
{
_cartStore = cartStore;
}
public async override Task<Empty> AddItem(AddItemRequest request, ServerCallContext context)
{
await _cartStore.AddItemAsync(request.UserId, request.Item.ProductId, request.Item.Quantity);
return Empty;
}
public override Task<Cart> GetCart(GetCartRequest request, ServerCallContext context)
{
return _cartStore.GetCartAsync(request.UserId);
}
public async override Task<Empty> EmptyCart(EmptyCartRequest request, ServerCallContext context)
{
await _cartStore.EmptyCartAsync(request.UserId);
return Empty;
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright 2020 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.
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Health.V1;
using static Grpc.Health.V1.Health;
using cartservice.cartstore;
namespace cartservice.services
{
internal class HealthCheckService : HealthBase
{
private ICartStore _cartStore { get; }
public HealthCheckService (ICartStore cartStore)
{
_cartStore = cartStore;
}
public override Task<HealthCheckResponse> Check(HealthCheckRequest request, ServerCallContext context)
{
Console.WriteLine ("Checking CartService Health");
return Task.FromResult(new HealthCheckResponse {
Status = _cartStore.Ping() ? HealthCheckResponse.Types.ServingStatus.Serving : HealthCheckResponse.Types.ServingStatus.NotServing
});
}
}
}

View File

@@ -0,0 +1,160 @@
// Copyright 2018 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.
using System;
using System.Threading.Tasks;
using Grpc.Net.Client;
using Hipstershop;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
using Xunit;
using static Hipstershop.CartService;
namespace cartservice.tests
{
public class CartServiceTests
{
private readonly IHostBuilder _host;
public CartServiceTests()
{
_host = new HostBuilder().ConfigureWebHost(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseTestServer();
});
}
[Fact]
public async Task GetItem_NoAddItemBefore_EmptyCartReturned()
{
// Setup test server and client
using var server = await _host.StartAsync();
var httpClient = server.GetTestClient();
string userId = Guid.NewGuid().ToString();
// Create a GRPC communication channel between the client and the server
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
{
HttpClient = httpClient
});
var cartClient = new CartServiceClient(channel);
var request = new GetCartRequest
{
UserId = userId,
};
var cart = await cartClient.GetCartAsync(request);
Assert.NotNull(cart);
// All grpc objects implement IEquitable, so we can compare equality with by-value semantics
Assert.Equal(new Cart(), cart);
}
[Fact]
public async Task AddItem_ItemExists_Updated()
{
// Setup test server and client
using var server = await _host.StartAsync();
var httpClient = server.GetTestClient();
string userId = Guid.NewGuid().ToString();
// Create a GRPC communication channel between the client and the server
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
{
HttpClient = httpClient
});
var client = new CartServiceClient(channel);
var request = new AddItemRequest
{
UserId = userId,
Item = new CartItem
{
ProductId = "1",
Quantity = 1
}
};
// First add - nothing should fail
await client.AddItemAsync(request);
// Second add of existing product - quantity should be updated
await client.AddItemAsync(request);
var getCartRequest = new GetCartRequest
{
UserId = userId
};
var cart = await client.GetCartAsync(getCartRequest);
Assert.NotNull(cart);
Assert.Equal(userId, cart.UserId);
Assert.Single(cart.Items);
Assert.Equal(2, cart.Items[0].Quantity);
// Cleanup
await client.EmptyCartAsync(new EmptyCartRequest { UserId = userId });
}
[Fact]
public async Task AddItem_New_Inserted()
{
// Setup test server and client
using var server = await _host.StartAsync();
var httpClient = server.GetTestClient();
string userId = Guid.NewGuid().ToString();
// Create a GRPC communication channel between the client and the server
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
{
HttpClient = httpClient
});
// Create a proxy object to work with the server
var client = new CartServiceClient(channel);
var request = new AddItemRequest
{
UserId = userId,
Item = new CartItem
{
ProductId = "1",
Quantity = 1
}
};
await client.AddItemAsync(request);
var getCartRequest = new GetCartRequest
{
UserId = userId
};
var cart = await client.GetCartAsync(getCartRequest);
Assert.NotNull(cart);
Assert.Equal(userId, cart.UserId);
Assert.Single(cart.Items);
await client.EmptyCartAsync(new EmptyCartRequest { UserId = userId });
cart = await client.GetCartAsync(getCartRequest);
Assert.Empty(cart.Items);
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\cartservice.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1 @@
vendor/

View File

@@ -0,0 +1,44 @@
# Copyright 2020 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.
# Define a default value so it's not empty if the builder fails to provide it
ARG BUILDPLATFORM=linux/amd64
FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder
ARG TARGETOS=linux
ARG TARGETARCH=amd64
WORKDIR /src
# restore dependencies
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Skaffold passes in debug-oriented compiler flags
ARG SKAFFOLD_GO_GCFLAGS
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -ldflags="-s -w" -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /checkoutservice .
FROM gcr.io/distroless/static
WORKDIR /src
COPY --from=builder /checkoutservice /src/checkoutservice
# Definition of this variable is used by 'skaffold debug' to identify a golang binary.
# Default behavior - a failure prints a stack trace for the current goroutine.
# See https://golang.org/pkg/runtime/
ENV GOTRACEBACK=single
EXPOSE 5050
ENTRYPOINT ["/src/checkoutservice"]

View File

@@ -0,0 +1,5 @@
# checkoutservice
Run the following command to restore dependencies to `vendor/` directory:
dep ensure --vendor-only

25
src/checkoutservice/genproto.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash -eu
#
# Copyright 2018 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.
# [START gke_checkoutservice_genproto]
PATH=$PATH:$(go env GOPATH)/bin
protodir=../../protos
outdir=./genproto
protoc --proto_path=$protodir --go_out=./$outdir --go_opt=paths=source_relative --go-grpc_out=./$outdir --go-grpc_opt=paths=source_relative $protodir/demo.proto
# [END gke_checkoutservice_genproto]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
module github.com/GoogleCloudPlatform/microservices-demo/src/checkoutservice
go 1.25.0
toolchain go1.26.1
require (
cloud.google.com/go/profiler v0.4.3
github.com/google/uuid v1.6.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.4
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0
go.opentelemetry.io/otel/sdk v1.42.0
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
)
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

132
src/checkoutservice/go.sum Normal file
View File

@@ -0,0 +1,132 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/profiler v0.4.3 h1:IY3QNKlr8VbXwGWHcZbJQsMA/83ZTH6uAHf8jYyj7OI=
cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0=
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 h1:dDbsTLIK7EzwUq36kCSAsk0slouq/S0tWHeeGi97cD8=
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846/go.mod h1:PP0g88Dz3C7hRAfbQCQggeWAXjuqGsNPLE4s7jh0RGU=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

394
src/checkoutservice/main.go Normal file
View File

@@ -0,0 +1,394 @@
// Copyright 2018 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 main
import (
"context"
"fmt"
"net"
"os"
"time"
"cloud.google.com/go/profiler"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/health"
"google.golang.org/grpc/status"
pb "github.com/GoogleCloudPlatform/microservices-demo/src/checkoutservice/genproto"
money "github.com/GoogleCloudPlatform/microservices-demo/src/checkoutservice/money"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
const (
listenPort = "5050"
usdCurrency = "USD"
)
var log *logrus.Logger
func init() {
log = logrus.New()
log.Level = logrus.DebugLevel
log.Formatter = &logrus.JSONFormatter{
FieldMap: logrus.FieldMap{
logrus.FieldKeyTime: "timestamp",
logrus.FieldKeyLevel: "severity",
logrus.FieldKeyMsg: "message",
},
TimestampFormat: time.RFC3339Nano,
}
log.Out = os.Stdout
}
type checkoutService struct {
pb.UnimplementedCheckoutServiceServer
productCatalogSvcAddr string
productCatalogSvcConn *grpc.ClientConn
cartSvcAddr string
cartSvcConn *grpc.ClientConn
currencySvcAddr string
currencySvcConn *grpc.ClientConn
shippingSvcAddr string
shippingSvcConn *grpc.ClientConn
emailSvcAddr string
emailSvcConn *grpc.ClientConn
paymentSvcAddr string
paymentSvcConn *grpc.ClientConn
}
func main() {
ctx := context.Background()
if os.Getenv("ENABLE_TRACING") == "1" {
log.Info("Tracing enabled.")
initTracing()
} else {
log.Info("Tracing disabled.")
}
if os.Getenv("ENABLE_PROFILER") == "1" {
log.Info("Profiling enabled.")
go initProfiling("checkoutservice", "1.0.0")
} else {
log.Info("Profiling disabled.")
}
port := listenPort
if os.Getenv("PORT") != "" {
port = os.Getenv("PORT")
}
svc := new(checkoutService)
mustMapEnv(&svc.shippingSvcAddr, "SHIPPING_SERVICE_ADDR")
mustMapEnv(&svc.productCatalogSvcAddr, "PRODUCT_CATALOG_SERVICE_ADDR")
mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR")
mustMapEnv(&svc.currencySvcAddr, "CURRENCY_SERVICE_ADDR")
mustMapEnv(&svc.emailSvcAddr, "EMAIL_SERVICE_ADDR")
mustMapEnv(&svc.paymentSvcAddr, "PAYMENT_SERVICE_ADDR")
mustConnGRPC(ctx, &svc.shippingSvcConn, svc.shippingSvcAddr)
mustConnGRPC(ctx, &svc.productCatalogSvcConn, svc.productCatalogSvcAddr)
mustConnGRPC(ctx, &svc.cartSvcConn, svc.cartSvcAddr)
mustConnGRPC(ctx, &svc.currencySvcConn, svc.currencySvcAddr)
mustConnGRPC(ctx, &svc.emailSvcConn, svc.emailSvcAddr)
mustConnGRPC(ctx, &svc.paymentSvcConn, svc.paymentSvcAddr)
log.Infof("service config: %+v", svc)
lis, err := net.Listen("tcp", fmt.Sprintf(":%s", port))
if err != nil {
log.Fatal(err)
}
var srv *grpc.Server
// Propagate trace context always
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, propagation.Baggage{}))
srv = grpc.NewServer(
grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
pb.RegisterCheckoutServiceServer(srv, svc)
healthcheck := health.NewServer()
healthpb.RegisterHealthServer(srv, healthcheck)
log.Infof("starting to listen on tcp: %q", lis.Addr().String())
err = srv.Serve(lis)
log.Fatal(err)
}
func initStats() {
//TODO(arbrown) Implement OpenTelemetry stats
}
func initTracing() {
var (
collectorAddr string
collectorConn *grpc.ClientConn
)
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
mustMapEnv(&collectorAddr, "COLLECTOR_SERVICE_ADDR")
mustConnGRPC(ctx, &collectorConn, collectorAddr)
exporter, err := otlptracegrpc.New(
ctx,
otlptracegrpc.WithGRPCConn(collectorConn))
if err != nil {
log.Warnf("warn: Failed to create trace exporter: %v", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
}
func initProfiling(service, version string) {
// TODO(ahmetb) this method is duplicated in other microservices using Go
// since they are not sharing packages.
for i := 1; i <= 3; i++ {
if err := profiler.Start(profiler.Config{
Service: service,
ServiceVersion: version,
// ProjectID must be set if not running on GCP.
// ProjectID: "my-project",
}); err != nil {
log.Warnf("failed to start profiler: %+v", err)
} else {
log.Info("started Stackdriver profiler")
return
}
d := time.Second * 10 * time.Duration(i)
log.Infof("sleeping %v to retry initializing Stackdriver profiler", d)
time.Sleep(d)
}
log.Warn("could not initialize Stackdriver profiler after retrying, giving up")
}
func mustMapEnv(target *string, envKey string) {
v := os.Getenv(envKey)
if v == "" {
panic(fmt.Sprintf("environment variable %q not set", envKey))
}
*target = v
}
func mustConnGRPC(ctx context.Context, conn **grpc.ClientConn, addr string) {
var err error
_, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
*conn, err = grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
if err != nil {
panic(errors.Wrapf(err, "grpc: failed to connect %s", addr))
}
}
func (cs *checkoutService) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil
}
func (cs *checkoutService) Watch(req *healthpb.HealthCheckRequest, ws healthpb.Health_WatchServer) error {
return status.Errorf(codes.Unimplemented, "health check via Watch not implemented")
}
func (cs *checkoutService) PlaceOrder(ctx context.Context, req *pb.PlaceOrderRequest) (*pb.PlaceOrderResponse, error) {
log.Infof("[PlaceOrder] user_id=%q user_currency=%q", req.UserId, req.UserCurrency)
orderID, err := uuid.NewUUID()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate order uuid")
}
prep, err := cs.prepareOrderItemsAndShippingQuoteFromCart(ctx, req.UserId, req.UserCurrency, req.Address)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
total := pb.Money{CurrencyCode: req.UserCurrency,
Units: 0,
Nanos: 0}
total = money.Must(money.Sum(total, *prep.shippingCostLocalized))
for _, it := range prep.orderItems {
multPrice := money.MultiplySlow(*it.Cost, uint32(it.GetItem().GetQuantity()))
total = money.Must(money.Sum(total, multPrice))
}
txID, err := cs.chargeCard(ctx, &total, req.CreditCard)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to charge card: %+v", err)
}
log.Infof("payment went through (transaction_id: %s)", txID)
shippingTrackingID, err := cs.shipOrder(ctx, req.Address, prep.cartItems)
if err != nil {
return nil, status.Errorf(codes.Unavailable, "shipping error: %+v", err)
}
_ = cs.emptyUserCart(ctx, req.UserId)
orderResult := &pb.OrderResult{
OrderId: orderID.String(),
ShippingTrackingId: shippingTrackingID,
ShippingCost: prep.shippingCostLocalized,
ShippingAddress: req.Address,
Items: prep.orderItems,
}
if err := cs.sendOrderConfirmation(ctx, req.Email, orderResult); err != nil {
log.Warnf("failed to send order confirmation to %q: %+v", req.Email, err)
} else {
log.Infof("order confirmation email sent to %q", req.Email)
}
resp := &pb.PlaceOrderResponse{Order: orderResult}
return resp, nil
}
type orderPrep struct {
orderItems []*pb.OrderItem
cartItems []*pb.CartItem
shippingCostLocalized *pb.Money
}
func (cs *checkoutService) prepareOrderItemsAndShippingQuoteFromCart(ctx context.Context, userID, userCurrency string, address *pb.Address) (orderPrep, error) {
var out orderPrep
cartItems, err := cs.getUserCart(ctx, userID)
if err != nil {
return out, fmt.Errorf("cart failure: %+v", err)
}
orderItems, err := cs.prepOrderItems(ctx, cartItems, userCurrency)
if err != nil {
return out, fmt.Errorf("failed to prepare order: %+v", err)
}
shippingUSD, err := cs.quoteShipping(ctx, address, cartItems)
if err != nil {
return out, fmt.Errorf("shipping quote failure: %+v", err)
}
shippingPrice, err := cs.convertCurrency(ctx, shippingUSD, userCurrency)
if err != nil {
return out, fmt.Errorf("failed to convert shipping cost to currency: %+v", err)
}
out.shippingCostLocalized = shippingPrice
out.cartItems = cartItems
out.orderItems = orderItems
return out, nil
}
func (cs *checkoutService) quoteShipping(ctx context.Context, address *pb.Address, items []*pb.CartItem) (*pb.Money, error) {
shippingQuote, err := pb.NewShippingServiceClient(cs.shippingSvcConn).
GetQuote(ctx, &pb.GetQuoteRequest{
Address: address,
Items: items})
if err != nil {
return nil, fmt.Errorf("failed to get shipping quote: %+v", err)
}
return shippingQuote.GetCostUsd(), nil
}
func (cs *checkoutService) getUserCart(ctx context.Context, userID string) ([]*pb.CartItem, error) {
cart, err := pb.NewCartServiceClient(cs.cartSvcConn).GetCart(ctx, &pb.GetCartRequest{UserId: userID})
if err != nil {
return nil, fmt.Errorf("failed to get user cart during checkout: %+v", err)
}
return cart.GetItems(), nil
}
func (cs *checkoutService) emptyUserCart(ctx context.Context, userID string) error {
if _, err := pb.NewCartServiceClient(cs.cartSvcConn).EmptyCart(ctx, &pb.EmptyCartRequest{UserId: userID}); err != nil {
return fmt.Errorf("failed to empty user cart during checkout: %+v", err)
}
return nil
}
func (cs *checkoutService) prepOrderItems(ctx context.Context, items []*pb.CartItem, userCurrency string) ([]*pb.OrderItem, error) {
out := make([]*pb.OrderItem, len(items))
cl := pb.NewProductCatalogServiceClient(cs.productCatalogSvcConn)
for i, item := range items {
product, err := cl.GetProduct(ctx, &pb.GetProductRequest{Id: item.GetProductId()})
if err != nil {
return nil, fmt.Errorf("failed to get product #%q", item.GetProductId())
}
price, err := cs.convertCurrency(ctx, product.GetPriceUsd(), userCurrency)
if err != nil {
return nil, fmt.Errorf("failed to convert price of %q to %s", item.GetProductId(), userCurrency)
}
out[i] = &pb.OrderItem{
Item: item,
Cost: price}
}
return out, nil
}
func (cs *checkoutService) convertCurrency(ctx context.Context, from *pb.Money, toCurrency string) (*pb.Money, error) {
result, err := pb.NewCurrencyServiceClient(cs.currencySvcConn).Convert(context.TODO(), &pb.CurrencyConversionRequest{
From: from,
ToCode: toCurrency})
if err != nil {
return nil, fmt.Errorf("failed to convert currency: %+v", err)
}
return result, err
}
func (cs *checkoutService) chargeCard(ctx context.Context, amount *pb.Money, paymentInfo *pb.CreditCardInfo) (string, error) {
paymentResp, err := pb.NewPaymentServiceClient(cs.paymentSvcConn).Charge(ctx, &pb.ChargeRequest{
Amount: amount,
CreditCard: paymentInfo})
if err != nil {
return "", fmt.Errorf("could not charge the card: %+v", err)
}
return paymentResp.GetTransactionId(), nil
}
func (cs *checkoutService) sendOrderConfirmation(ctx context.Context, email string, order *pb.OrderResult) error {
_, err := pb.NewEmailServiceClient(cs.emailSvcConn).SendOrderConfirmation(ctx, &pb.SendOrderConfirmationRequest{
Email: email,
Order: order})
return err
}
func (cs *checkoutService) shipOrder(ctx context.Context, address *pb.Address, items []*pb.CartItem) (string, error) {
resp, err := pb.NewShippingServiceClient(cs.shippingSvcConn).ShipOrder(ctx, &pb.ShipOrderRequest{
Address: address,
Items: items})
if err != nil {
return "", fmt.Errorf("shipment failed: %+v", err)
}
return resp.GetTrackingId(), nil
}

View File

@@ -0,0 +1,132 @@
// Copyright 2018 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 money
import (
"errors"
pb "github.com/GoogleCloudPlatform/microservices-demo/src/checkoutservice/genproto"
)
const (
nanosMin = -999999999
nanosMax = +999999999
nanosMod = 1000000000
)
var (
ErrInvalidValue = errors.New("one of the specified money values is invalid")
ErrMismatchingCurrency = errors.New("mismatching currency codes")
)
// IsValid checks if specified value has a valid units/nanos signs and ranges.
func IsValid(m pb.Money) bool {
return signMatches(m) && validNanos(m.GetNanos())
}
func signMatches(m pb.Money) bool {
return m.GetNanos() == 0 || m.GetUnits() == 0 || (m.GetNanos() < 0) == (m.GetUnits() < 0)
}
func validNanos(nanos int32) bool { return nanosMin <= nanos && nanos <= nanosMax }
// IsZero returns true if the specified money value is equal to zero.
func IsZero(m pb.Money) bool { return m.GetUnits() == 0 && m.GetNanos() == 0 }
// IsPositive returns true if the specified money value is valid and is
// positive.
func IsPositive(m pb.Money) bool {
return IsValid(m) && m.GetUnits() > 0 || (m.GetUnits() == 0 && m.GetNanos() > 0)
}
// IsNegative returns true if the specified money value is valid and is
// negative.
func IsNegative(m pb.Money) bool {
return IsValid(m) && m.GetUnits() < 0 || (m.GetUnits() == 0 && m.GetNanos() < 0)
}
// AreSameCurrency returns true if values l and r have a currency code and
// they are the same values.
func AreSameCurrency(l, r pb.Money) bool {
return l.GetCurrencyCode() == r.GetCurrencyCode() && l.GetCurrencyCode() != ""
}
// AreEquals returns true if values l and r are the equal, including the
// currency. This does not check validity of the provided values.
func AreEquals(l, r pb.Money) bool {
return l.GetCurrencyCode() == r.GetCurrencyCode() &&
l.GetUnits() == r.GetUnits() && l.GetNanos() == r.GetNanos()
}
// Negate returns the same amount with the sign negated.
func Negate(m pb.Money) pb.Money {
return pb.Money{
Units: -m.GetUnits(),
Nanos: -m.GetNanos(),
CurrencyCode: m.GetCurrencyCode()}
}
// Must panics if the given error is not nil. This can be used with other
// functions like: "m := Must(Sum(a,b))".
func Must(v pb.Money, err error) pb.Money {
if err != nil {
panic(err)
}
return v
}
// Sum adds two values. Returns an error if one of the values are invalid or
// currency codes are not matching (unless currency code is unspecified for
// both).
func Sum(l, r pb.Money) (pb.Money, error) {
if !IsValid(l) || !IsValid(r) {
return pb.Money{}, ErrInvalidValue
} else if l.GetCurrencyCode() != r.GetCurrencyCode() {
return pb.Money{}, ErrMismatchingCurrency
}
units := l.GetUnits() + r.GetUnits()
nanos := l.GetNanos() + r.GetNanos()
if (units == 0 && nanos == 0) || (units > 0 && nanos >= 0) || (units < 0 && nanos <= 0) {
// same sign <units, nanos>
units += int64(nanos / nanosMod)
nanos = nanos % nanosMod
} else {
// different sign. nanos guaranteed to not to go over the limit
if units > 0 {
units--
nanos += nanosMod
} else {
units++
nanos -= nanosMod
}
}
return pb.Money{
Units: units,
Nanos: nanos,
CurrencyCode: l.GetCurrencyCode()}, nil
}
// MultiplySlow is a slow multiplication operation done through adding the value
// to itself n-1 times.
func MultiplySlow(m pb.Money, n uint32) pb.Money {
out := m
for n > 1 {
out = Must(Sum(out, m))
n--
}
return out
}

View File

@@ -0,0 +1,245 @@
// Copyright 2018 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 money
import (
"fmt"
"reflect"
"testing"
pb "github.com/GoogleCloudPlatform/microservices-demo/src/checkoutservice/genproto"
)
func mmc(u int64, n int32, c string) pb.Money { return pb.Money{Units: u, Nanos: n, CurrencyCode: c} }
func mm(u int64, n int32) pb.Money { return mmc(u, n, "") }
func TestIsValid(t *testing.T) {
tests := []struct {
name string
in pb.Money
want bool
}{
{"valid -/-", mm(-981273891273, -999999999), true},
{"invalid -/+", mm(-981273891273, +999999999), false},
{"valid +/+", mm(981273891273, 999999999), true},
{"invalid +/-", mm(981273891273, -999999999), false},
{"invalid +/+overflow", mm(3, 1000000000), false},
{"invalid +/-overflow", mm(3, -1000000000), false},
{"invalid -/+overflow", mm(-3, 1000000000), false},
{"invalid -/-overflow", mm(-3, -1000000000), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValid(tt.in); got != tt.want {
t.Errorf("IsValid(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestIsZero(t *testing.T) {
tests := []struct {
name string
in pb.Money
want bool
}{
{"zero", mm(0, 0), true},
{"not-zero (-/+)", mm(-1, +1), false},
{"not-zero (-/-)", mm(-1, -1), false},
{"not-zero (+/+)", mm(+1, +1), false},
{"not-zero (+/-)", mm(+1, -1), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsZero(tt.in); got != tt.want {
t.Errorf("IsZero(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestIsPositive(t *testing.T) {
tests := []struct {
name string
in pb.Money
want bool
}{
{"zero", mm(0, 0), false},
{"positive (+/+)", mm(+1, +1), true},
{"invalid (-/+)", mm(-1, +1), false},
{"negative (-/-)", mm(-1, -1), false},
{"invalid (+/-)", mm(+1, -1), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsPositive(tt.in); got != tt.want {
t.Errorf("IsPositive(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestIsNegative(t *testing.T) {
tests := []struct {
name string
in pb.Money
want bool
}{
{"zero", mm(0, 0), false},
{"positive (+/+)", mm(+1, +1), false},
{"invalid (-/+)", mm(-1, +1), false},
{"negative (-/-)", mm(-1, -1), true},
{"invalid (+/-)", mm(+1, -1), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsNegative(tt.in); got != tt.want {
t.Errorf("IsNegative(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestAreSameCurrency(t *testing.T) {
type args struct {
l pb.Money
r pb.Money
}
tests := []struct {
name string
args args
want bool
}{
{"both empty currency", args{mmc(1, 0, ""), mmc(2, 0, "")}, false},
{"left empty currency", args{mmc(1, 0, ""), mmc(2, 0, "USD")}, false},
{"right empty currency", args{mmc(1, 0, "USD"), mmc(2, 0, "")}, false},
{"mismatching", args{mmc(1, 0, "USD"), mmc(2, 0, "CAD")}, false},
{"matching", args{mmc(1, 0, "USD"), mmc(2, 0, "USD")}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := AreSameCurrency(tt.args.l, tt.args.r); got != tt.want {
t.Errorf("AreSameCurrency([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want)
}
})
}
}
func TestAreEquals(t *testing.T) {
type args struct {
l pb.Money
r pb.Money
}
tests := []struct {
name string
args args
want bool
}{
{"equals", args{mmc(1, 2, "USD"), mmc(1, 2, "USD")}, true},
{"mismatching currency", args{mmc(1, 2, "USD"), mmc(1, 2, "CAD")}, false},
{"mismatching units", args{mmc(10, 20, "USD"), mmc(1, 20, "USD")}, false},
{"mismatching nanos", args{mmc(1, 2, "USD"), mmc(1, 20, "USD")}, false},
{"negated", args{mmc(1, 2, "USD"), mmc(-1, -2, "USD")}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := AreEquals(tt.args.l, tt.args.r); got != tt.want {
t.Errorf("AreEquals([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want)
}
})
}
}
func TestNegate(t *testing.T) {
tests := []struct {
name string
in pb.Money
want pb.Money
}{
{"zero", mm(0, 0), mm(0, 0)},
{"negative", mm(-1, -200), mm(1, 200)},
{"positive", mm(1, 200), mm(-1, -200)},
{"carries currency code", mmc(0, 0, "XXX"), mmc(0, 0, "XXX")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Negate(tt.in); !AreEquals(got, tt.want) {
t.Errorf("Negate([%v]) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestMust_pass(t *testing.T) {
v := Must(mm(2, 3), nil)
if !AreEquals(v, mm(2, 3)) {
t.Errorf("returned the wrong value: %v", v)
}
}
func TestMust_panic(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Logf("panic captured: %v", r)
}
}()
Must(mm(2, 3), fmt.Errorf("some error"))
t.Fatal("this should not have executed due to the panic above")
}
func TestSum(t *testing.T) {
type args struct {
l pb.Money
r pb.Money
}
tests := []struct {
name string
args args
want pb.Money
wantErr error
}{
{"0+0=0", args{mm(0, 0), mm(0, 0)}, mm(0, 0), nil},
{"Error: currency code on left", args{mmc(0, 0, "XXX"), mm(0, 0)}, mm(0, 0), ErrMismatchingCurrency},
{"Error: currency code on right", args{mm(0, 0), mmc(0, 0, "YYY")}, mm(0, 0), ErrMismatchingCurrency},
{"Error: currency code mismatch", args{mmc(0, 0, "AAA"), mmc(0, 0, "BBB")}, mm(0, 0), ErrMismatchingCurrency},
{"Error: invalid +/-", args{mm(+1, -1), mm(0, 0)}, mm(0, 0), ErrInvalidValue},
{"Error: invalid -/+", args{mm(0, 0), mm(-1, +2)}, mm(0, 0), ErrInvalidValue},
{"Error: invalid nanos", args{mm(0, 1000000000), mm(1, 0)}, mm(0, 0), ErrInvalidValue},
{"both positive (no carry)", args{mm(2, 200000000), mm(2, 200000000)}, mm(4, 400000000), nil},
{"both positive (nanos=max)", args{mm(2, 111111111), mm(2, 888888888)}, mm(4, 999999999), nil},
{"both positive (carry)", args{mm(2, 200000000), mm(2, 900000000)}, mm(5, 100000000), nil},
{"both negative (no carry)", args{mm(-2, -200000000), mm(-2, -200000000)}, mm(-4, -400000000), nil},
{"both negative (carry)", args{mm(-2, -200000000), mm(-2, -900000000)}, mm(-5, -100000000), nil},
{"mixed (larger positive, just decimals)", args{mm(11, 0), mm(-2, 0)}, mm(9, 0), nil},
{"mixed (larger negative, just decimals)", args{mm(-11, 0), mm(2, 0)}, mm(-9, 0), nil},
{"mixed (larger positive, no borrow)", args{mm(11, 100000000), mm(-2, -100000000)}, mm(9, 0), nil},
{"mixed (larger positive, with borrow)", args{mm(11, 100000000), mm(-2, -9000000 /*.09*/)}, mm(9, 91000000 /*.091*/), nil},
{"mixed (larger negative, no borrow)", args{mm(-11, -100000000), mm(2, 100000000)}, mm(-9, 0), nil},
{"mixed (larger negative, with borrow)", args{mm(-11, -100000000), mm(2, 9000000 /*.09*/)}, mm(-9, -91000000 /*.091*/), nil},
{"0+negative", args{mm(0, 0), mm(-2, -100000000)}, mm(-2, -100000000), nil},
{"negative+0", args{mm(-2, -100000000), mm(0, 0)}, mm(-2, -100000000), nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Sum(tt.args.l, tt.args.r)
if err != tt.wantErr {
t.Errorf("Sum([%v],[%v]): expected err=\"%v\" got=\"%v\"", tt.args.l, tt.args.r, tt.wantErr, err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Sum([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
<suppressions>
<!-- Upstream defaults -->
<suppress files="node_modules[\\/].*" checks=".*"/>
<suppress files="node[\\/].*" checks=".*"/>
<suppress files="build[\\/].*" checks=".*"/>
<suppress files="target[\\/].*" checks=".*"/>
<suppress files=".+\.(jar|git|ico|p12|gif|jks|jpg|svg|log)" checks="NoHttp"/>
<!-- Platform-scaffolded paths that use internal Kubernetes service URLs (http only) -->
<suppress files="k6[\\/].*" checks="NoHttp"/>
<suppress files="overlays[\\/].*" checks="NoHttp"/>
<suppress files="docs[\\/].*" checks="NoHttp"/>
<suppress files="config[\\/].*" checks="NoHttp"/>
</suppressions>

View File

@@ -0,0 +1,2 @@
client.js
node_modules/

1
src/currencyservice/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -0,0 +1,45 @@
# Copyright 2020 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.
# Define a default value so it's not empty if the builder fails to provide it
ARG BUILDPLATFORM=linux/amd64
FROM --platform=$BUILDPLATFORM node:20.20.1-alpine@sha256:b88333c42c23fbd91596ebd7fd10de239cedab9617de04142dde7315e3bc0afa AS builder
# Some packages (e.g. @google-cloud/profiler) require additional
# deps for post-install scripts
RUN apk add --update --no-cache \
python3 \
make \
g++
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
FROM alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
RUN apk add --no-cache nodejs
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY . .
EXPOSE 7000
ENTRYPOINT [ "node", "server.js" ]

View File

@@ -0,0 +1,68 @@
/*
*
* Copyright 2015 gRPC authors.
*
* 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.
*
*/
require('@google-cloud/trace-agent').start();
const path = require('path');
const grpc = require('grpc');
const pino = require('pino');
const PROTO_PATH = path.join(__dirname, './proto/demo.proto');
const PORT = 7000;
const shopProto = grpc.load(PROTO_PATH).hipstershop;
const client = new shopProto.CurrencyService(`localhost:${PORT}`,
grpc.credentials.createInsecure());
const logger = pino({
name: 'currencyservice-client',
messageKey: 'message',
formatters: {
level (logLevelString, logLevelNum) {
return { severity: logLevelString }
}
}
});
const request = {
from: {
currency_code: 'CHF',
units: 300,
nanos: 0
},
to_code: 'EUR'
};
function _moneyToString (m) {
return `${m.units}.${m.nanos.toString().padStart(9,'0')} ${m.currency_code}`;
}
client.getSupportedCurrencies({}, (err, response) => {
if (err) {
logger.error(`Error in getSupportedCurrencies: ${err}`);
} else {
logger.info(`Currency codes: ${response.currency_codes}`);
}
});
client.convert(request, (err, response) => {
if (err) {
logger.error(`Error in convert: ${err}`);
} else {
logger.log(`Convert: ${_moneyToString(request.from)} to ${_moneyToString(response)}`);
}
});

View File

@@ -0,0 +1,35 @@
{
"EUR": "1.0",
"USD": "1.1305",
"JPY": "126.40",
"BGN": "1.9558",
"CZK": "25.592",
"DKK": "7.4609",
"GBP": "0.85970",
"HUF": "315.51",
"PLN": "4.2996",
"RON": "4.7463",
"SEK": "10.5375",
"CHF": "1.1360",
"ISK": "136.80",
"NOK": "9.8040",
"HRK": "7.4210",
"RUB": "74.4208",
"TRY": "6.1247",
"AUD": "1.6072",
"BRL": "4.2682",
"CAD": "1.5128",
"CNY": "7.5857",
"HKD": "8.8743",
"IDR": "15999.40",
"ILS": "4.0875",
"INR": "79.4320",
"KRW": "1275.05",
"MXN": "21.7999",
"MYR": "4.6289",
"NZD": "1.6679",
"PHP": "59.083",
"SGD": "1.5349",
"THB": "36.012",
"ZAR": "16.0583"
}

23
src/currencyservice/genproto.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash -eu
#
# Copyright 2018 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.
# [START gke_currencyservice_genproto]
# protos are loaded dynamically for node, simply copies over the proto.
mkdir -p proto
cp -r ../../protos/* ./proto
# [END gke_currencyservice_genproto]

3563
src/currencyservice/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
{
"name": "grpc-currency-service",
"version": "0.1.0",
"description": "A gRPC currency conversion microservice",
"repository": "https://github.com/GoogleCloudPlatform/microservices-demo",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"license": "Apache-2.0",
"dependencies": {
"@google-cloud/profiler": "6.0.4",
"@google-cloud/trace-agent": "8.0.0",
"@grpc/grpc-js": "1.14.3",
"@grpc/proto-loader": "0.8.0",
"async": "3.2.6",
"google-protobuf": "4.0.2",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/exporter-otlp-grpc": "0.26.0",
"@opentelemetry/instrumentation-grpc": "0.213.0",
"@opentelemetry/resources": "2.6.0",
"@opentelemetry/semantic-conventions": "1.40.0",
"@opentelemetry/sdk-trace-base": "2.6.0",
"@opentelemetry/sdk-node": "0.213.0",
"pino": "10.3.1",
"xml2js": "0.6.2"
}
}

View File

@@ -0,0 +1,260 @@
// Copyright 2020 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.
syntax = "proto3";
package hipstershop;
// -----------------Cart service-----------------
service CartService {
rpc AddItem(AddItemRequest) returns (Empty) {}
rpc GetCart(GetCartRequest) returns (Cart) {}
rpc EmptyCart(EmptyCartRequest) returns (Empty) {}
}
message CartItem {
string product_id = 1;
int32 quantity = 2;
}
message AddItemRequest {
string user_id = 1;
CartItem item = 2;
}
message EmptyCartRequest {
string user_id = 1;
}
message GetCartRequest {
string user_id = 1;
}
message Cart {
string user_id = 1;
repeated CartItem items = 2;
}
message Empty {}
// ---------------Recommendation service----------
service RecommendationService {
rpc ListRecommendations(ListRecommendationsRequest) returns (ListRecommendationsResponse){}
}
message ListRecommendationsRequest {
string user_id = 1;
repeated string product_ids = 2;
}
message ListRecommendationsResponse {
repeated string product_ids = 1;
}
// ---------------Product Catalog----------------
service ProductCatalogService {
rpc ListProducts(Empty) returns (ListProductsResponse) {}
rpc GetProduct(GetProductRequest) returns (Product) {}
rpc SearchProducts(SearchProductsRequest) returns (SearchProductsResponse) {}
}
message Product {
string id = 1;
string name = 2;
string description = 3;
string picture = 4;
Money price_usd = 5;
// Categories such as "clothing" or "kitchen" that can be used to look up
// other related products.
repeated string categories = 6;
}
message ListProductsResponse {
repeated Product products = 1;
}
message GetProductRequest {
string id = 1;
}
message SearchProductsRequest {
string query = 1;
}
message SearchProductsResponse {
repeated Product results = 1;
}
// ---------------Shipping Service----------
service ShippingService {
rpc GetQuote(GetQuoteRequest) returns (GetQuoteResponse) {}
rpc ShipOrder(ShipOrderRequest) returns (ShipOrderResponse) {}
}
message GetQuoteRequest {
Address address = 1;
repeated CartItem items = 2;
}
message GetQuoteResponse {
Money cost_usd = 1;
}
message ShipOrderRequest {
Address address = 1;
repeated CartItem items = 2;
}
message ShipOrderResponse {
string tracking_id = 1;
}
message Address {
string street_address = 1;
string city = 2;
string state = 3;
string country = 4;
int32 zip_code = 5;
}
// -----------------Currency service-----------------
service CurrencyService {
rpc GetSupportedCurrencies(Empty) returns (GetSupportedCurrenciesResponse) {}
rpc Convert(CurrencyConversionRequest) returns (Money) {}
}
// Represents an amount of money with its currency type.
message Money {
// The 3-letter currency code defined in ISO 4217.
string currency_code = 1;
// The whole units of the amount.
// For example if `currencyCode` is `"USD"`, then 1 unit is one US dollar.
int64 units = 2;
// Number of nano (10^-9) units of the amount.
// The value must be between -999,999,999 and +999,999,999 inclusive.
// If `units` is positive, `nanos` must be positive or zero.
// If `units` is zero, `nanos` can be positive, zero, or negative.
// If `units` is negative, `nanos` must be negative or zero.
// For example $-1.75 is represented as `units`=-1 and `nanos`=-750,000,000.
int32 nanos = 3;
}
message GetSupportedCurrenciesResponse {
// The 3-letter currency code defined in ISO 4217.
repeated string currency_codes = 1;
}
message CurrencyConversionRequest {
Money from = 1;
// The 3-letter currency code defined in ISO 4217.
string to_code = 2;
}
// -------------Payment service-----------------
service PaymentService {
rpc Charge(ChargeRequest) returns (ChargeResponse) {}
}
message CreditCardInfo {
string credit_card_number = 1;
int32 credit_card_cvv = 2;
int32 credit_card_expiration_year = 3;
int32 credit_card_expiration_month = 4;
}
message ChargeRequest {
Money amount = 1;
CreditCardInfo credit_card = 2;
}
message ChargeResponse {
string transaction_id = 1;
}
// -------------Email service-----------------
service EmailService {
rpc SendOrderConfirmation(SendOrderConfirmationRequest) returns (Empty) {}
}
message OrderItem {
CartItem item = 1;
Money cost = 2;
}
message OrderResult {
string order_id = 1;
string shipping_tracking_id = 2;
Money shipping_cost = 3;
Address shipping_address = 4;
repeated OrderItem items = 5;
}
message SendOrderConfirmationRequest {
string email = 1;
OrderResult order = 2;
}
// -------------Checkout service-----------------
service CheckoutService {
rpc PlaceOrder(PlaceOrderRequest) returns (PlaceOrderResponse) {}
}
message PlaceOrderRequest {
string user_id = 1;
string user_currency = 2;
Address address = 3;
string email = 5;
CreditCardInfo credit_card = 6;
}
message PlaceOrderResponse {
OrderResult order = 1;
}
// ------------Ad service------------------
service AdService {
rpc GetAds(AdRequest) returns (AdResponse) {}
}
message AdRequest {
// List of important key words from the current page describing the context.
repeated string context_keys = 1;
}
message AdResponse {
repeated Ad ads = 1;
}
message Ad {
// url to redirect to when an ad is clicked.
string redirect_url = 1;
// short advertisement text to display.
string text = 2;
}

View File

@@ -0,0 +1,43 @@
// Copyright 2015 The gRPC Authors
//
// 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.
// The canonical version of this proto can be found at
// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto
syntax = "proto3";
package grpc.health.v1;
option csharp_namespace = "Grpc.Health.V1";
option go_package = "google.golang.org/grpc/health/grpc_health_v1";
option java_multiple_files = true;
option java_outer_classname = "HealthProto";
option java_package = "io.grpc.health.v1";
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
}
ServingStatus status = 1;
}
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2018 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.
*/
const pino = require('pino');
const logger = pino({
name: 'currencyservice-server',
messageKey: 'message',
formatters: {
level (logLevelString, logLevelNum) {
return { severity: logLevelString }
}
}
});
if(process.env.DISABLE_PROFILER) {
logger.info("Profiler disabled.")
}
else {
logger.info("Profiler enabled.")
require('@google-cloud/profiler').start({
serviceContext: {
service: 'currencyservice',
version: '1.0.0'
}
});
}
// Register GRPC OTel Instrumentation for trace propagation
// regardless of whether tracing is emitted.
const { GrpcInstrumentation } = require('@opentelemetry/instrumentation-grpc');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
registerInstrumentations({
instrumentations: [new GrpcInstrumentation()]
});
if(process.env.ENABLE_TRACING == "1") {
logger.info("Tracing enabled.")
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
const opentelemetry = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-otlp-grpc');
const collectorUrl = process.env.COLLECTOR_SERVICE_ADDR;
const traceExporter = new OTLPTraceExporter({url: collectorUrl});
const sdk = new opentelemetry.NodeSDK({
resource: resourceFromAttributes({
[ ATTR_SERVICE_NAME ]: process.env.OTEL_SERVICE_NAME || 'currencyservice',
}),
traceExporter: traceExporter,
});
sdk.start()
}
else {
logger.info("Tracing disabled.")
}
const path = require('path');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const MAIN_PROTO_PATH = path.join(__dirname, './proto/demo.proto');
const HEALTH_PROTO_PATH = path.join(__dirname, './proto/grpc/health/v1/health.proto');
const PORT = process.env.PORT;
const shopProto = _loadProto(MAIN_PROTO_PATH).hipstershop;
const healthProto = _loadProto(HEALTH_PROTO_PATH).grpc.health.v1;
/**
* Helper function that loads a protobuf file.
*/
function _loadProto (path) {
const packageDefinition = protoLoader.loadSync(
path,
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
);
return grpc.loadPackageDefinition(packageDefinition);
}
/**
* Helper function that gets currency data from a stored JSON file
* Uses public data from European Central Bank
*/
function _getCurrencyData (callback) {
const data = require('./data/currency_conversion.json');
callback(data);
}
/**
* Helper function that handles decimal/fractional carrying
*/
function _carry (amount) {
const fractionSize = Math.pow(10, 9);
amount.nanos += (amount.units % 1) * fractionSize;
amount.units = Math.floor(amount.units) + Math.floor(amount.nanos / fractionSize);
amount.nanos = amount.nanos % fractionSize;
return amount;
}
/**
* Lists the supported currencies
*/
function getSupportedCurrencies (call, callback) {
logger.info('Getting supported currencies...');
_getCurrencyData((data) => {
callback(null, {currency_codes: Object.keys(data)});
});
}
/**
* Converts between currencies
*/
function convert (call, callback) {
try {
_getCurrencyData((data) => {
const request = call.request;
// Convert: from_currency --> EUR
const from = request.from;
const euros = _carry({
units: from.units / data[from.currency_code],
nanos: from.nanos / data[from.currency_code]
});
euros.nanos = Math.round(euros.nanos);
// Convert: EUR --> to_currency
const result = _carry({
units: euros.units * data[request.to_code],
nanos: euros.nanos * data[request.to_code]
});
result.units = Math.floor(result.units);
result.nanos = Math.floor(result.nanos);
result.currency_code = request.to_code;
logger.info(`conversion request successful`);
callback(null, result);
});
} catch (err) {
logger.error(`conversion request failed: ${err}`);
callback(err.message);
}
}
/**
* Endpoint for health checks
*/
function check (call, callback) {
callback(null, { status: 'SERVING' });
}
/**
* Starts an RPC server that receives requests for the
* CurrencyConverter service at the sample server port
*/
function main () {
logger.info(`Starting gRPC server on port ${PORT}...`);
const server = new grpc.Server();
server.addService(shopProto.CurrencyService.service, {getSupportedCurrencies, convert});
server.addService(healthProto.Health.service, {check});
server.bindAsync(
`[::]:${PORT}`,
grpc.ServerCredentials.createInsecure(),
function() {
logger.info(`CurrencyService gRPC server started on port ${PORT}`);
server.start();
},
);
}
main();

View File

@@ -0,0 +1,54 @@
# Copyright 2020 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.
# Define a default value so it's not empty if the builder fails to provide it
ARG BUILDPLATFORM=linux/amd64
FROM --platform=$BUILDPLATFORM python:3.14.3-alpine@sha256:faee120f7885a06fcc9677922331391fa690d911c020abb9e8025ff3d908e510 AS base
FROM base AS builder
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
RUN apk update \
&& apk add --no-cache g++ linux-headers \
&& rm -rf /var/cache/apk/*
# get packages
COPY requirements.txt .
RUN pip install -r requirements.txt
FROM base
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Enable Profiler
ENV ENABLE_PROFILER=1
RUN apk update \
&& apk add --no-cache libstdc++ \
&& rm -rf /var/cache/apk/*
WORKDIR /email_server
# Grab packages from builder
COPY --from=builder /usr/local/lib/python3.14/ /usr/local/lib/python3.14/
# Add the application
COPY . .
EXPOSE 8080
ENTRYPOINT [ "python", "email_server.py" ]

121
src/emailservice/demo_pb2.py Executable file

File diff suppressed because one or more lines are too long

822
src/emailservice/demo_pb2_grpc.py Executable file
View File

@@ -0,0 +1,822 @@
#!/usr/bin/python
#
# Copyright 2018 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
#
# https://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.
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import demo_pb2 as demo__pb2
class CartServiceStub(object):
"""-----------------Cart service-----------------
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.AddItem = channel.unary_unary(
'/hipstershop.CartService/AddItem',
request_serializer=demo__pb2.AddItemRequest.SerializeToString,
response_deserializer=demo__pb2.Empty.FromString,
)
self.GetCart = channel.unary_unary(
'/hipstershop.CartService/GetCart',
request_serializer=demo__pb2.GetCartRequest.SerializeToString,
response_deserializer=demo__pb2.Cart.FromString,
)
self.EmptyCart = channel.unary_unary(
'/hipstershop.CartService/EmptyCart',
request_serializer=demo__pb2.EmptyCartRequest.SerializeToString,
response_deserializer=demo__pb2.Empty.FromString,
)
class CartServiceServicer(object):
"""-----------------Cart service-----------------
"""
def AddItem(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetCart(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def EmptyCart(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_CartServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'AddItem': grpc.unary_unary_rpc_method_handler(
servicer.AddItem,
request_deserializer=demo__pb2.AddItemRequest.FromString,
response_serializer=demo__pb2.Empty.SerializeToString,
),
'GetCart': grpc.unary_unary_rpc_method_handler(
servicer.GetCart,
request_deserializer=demo__pb2.GetCartRequest.FromString,
response_serializer=demo__pb2.Cart.SerializeToString,
),
'EmptyCart': grpc.unary_unary_rpc_method_handler(
servicer.EmptyCart,
request_deserializer=demo__pb2.EmptyCartRequest.FromString,
response_serializer=demo__pb2.Empty.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'hipstershop.CartService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class CartService(object):
"""-----------------Cart service-----------------
"""
@staticmethod
def AddItem(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.CartService/AddItem',
demo__pb2.AddItemRequest.SerializeToString,
demo__pb2.Empty.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetCart(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.CartService/GetCart',
demo__pb2.GetCartRequest.SerializeToString,
demo__pb2.Cart.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def EmptyCart(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.CartService/EmptyCart',
demo__pb2.EmptyCartRequest.SerializeToString,
demo__pb2.Empty.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
class RecommendationServiceStub(object):
"""---------------Recommendation service----------
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.ListRecommendations = channel.unary_unary(
'/hipstershop.RecommendationService/ListRecommendations',
request_serializer=demo__pb2.ListRecommendationsRequest.SerializeToString,
response_deserializer=demo__pb2.ListRecommendationsResponse.FromString,
)
class RecommendationServiceServicer(object):
"""---------------Recommendation service----------
"""
def ListRecommendations(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_RecommendationServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'ListRecommendations': grpc.unary_unary_rpc_method_handler(
servicer.ListRecommendations,
request_deserializer=demo__pb2.ListRecommendationsRequest.FromString,
response_serializer=demo__pb2.ListRecommendationsResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'hipstershop.RecommendationService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class RecommendationService(object):
"""---------------Recommendation service----------
"""
@staticmethod
def ListRecommendations(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.RecommendationService/ListRecommendations',
demo__pb2.ListRecommendationsRequest.SerializeToString,
demo__pb2.ListRecommendationsResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
class ProductCatalogServiceStub(object):
"""---------------Product Catalog----------------
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.ListProducts = channel.unary_unary(
'/hipstershop.ProductCatalogService/ListProducts',
request_serializer=demo__pb2.Empty.SerializeToString,
response_deserializer=demo__pb2.ListProductsResponse.FromString,
)
self.GetProduct = channel.unary_unary(
'/hipstershop.ProductCatalogService/GetProduct',
request_serializer=demo__pb2.GetProductRequest.SerializeToString,
response_deserializer=demo__pb2.Product.FromString,
)
self.SearchProducts = channel.unary_unary(
'/hipstershop.ProductCatalogService/SearchProducts',
request_serializer=demo__pb2.SearchProductsRequest.SerializeToString,
response_deserializer=demo__pb2.SearchProductsResponse.FromString,
)
class ProductCatalogServiceServicer(object):
"""---------------Product Catalog----------------
"""
def ListProducts(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetProduct(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def SearchProducts(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_ProductCatalogServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'ListProducts': grpc.unary_unary_rpc_method_handler(
servicer.ListProducts,
request_deserializer=demo__pb2.Empty.FromString,
response_serializer=demo__pb2.ListProductsResponse.SerializeToString,
),
'GetProduct': grpc.unary_unary_rpc_method_handler(
servicer.GetProduct,
request_deserializer=demo__pb2.GetProductRequest.FromString,
response_serializer=demo__pb2.Product.SerializeToString,
),
'SearchProducts': grpc.unary_unary_rpc_method_handler(
servicer.SearchProducts,
request_deserializer=demo__pb2.SearchProductsRequest.FromString,
response_serializer=demo__pb2.SearchProductsResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'hipstershop.ProductCatalogService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class ProductCatalogService(object):
"""---------------Product Catalog----------------
"""
@staticmethod
def ListProducts(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.ProductCatalogService/ListProducts',
demo__pb2.Empty.SerializeToString,
demo__pb2.ListProductsResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetProduct(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.ProductCatalogService/GetProduct',
demo__pb2.GetProductRequest.SerializeToString,
demo__pb2.Product.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def SearchProducts(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.ProductCatalogService/SearchProducts',
demo__pb2.SearchProductsRequest.SerializeToString,
demo__pb2.SearchProductsResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
class ShippingServiceStub(object):
"""---------------Shipping Service----------
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.GetQuote = channel.unary_unary(
'/hipstershop.ShippingService/GetQuote',
request_serializer=demo__pb2.GetQuoteRequest.SerializeToString,
response_deserializer=demo__pb2.GetQuoteResponse.FromString,
)
self.ShipOrder = channel.unary_unary(
'/hipstershop.ShippingService/ShipOrder',
request_serializer=demo__pb2.ShipOrderRequest.SerializeToString,
response_deserializer=demo__pb2.ShipOrderResponse.FromString,
)
class ShippingServiceServicer(object):
"""---------------Shipping Service----------
"""
def GetQuote(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def ShipOrder(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_ShippingServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'GetQuote': grpc.unary_unary_rpc_method_handler(
servicer.GetQuote,
request_deserializer=demo__pb2.GetQuoteRequest.FromString,
response_serializer=demo__pb2.GetQuoteResponse.SerializeToString,
),
'ShipOrder': grpc.unary_unary_rpc_method_handler(
servicer.ShipOrder,
request_deserializer=demo__pb2.ShipOrderRequest.FromString,
response_serializer=demo__pb2.ShipOrderResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'hipstershop.ShippingService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class ShippingService(object):
"""---------------Shipping Service----------
"""
@staticmethod
def GetQuote(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.ShippingService/GetQuote',
demo__pb2.GetQuoteRequest.SerializeToString,
demo__pb2.GetQuoteResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def ShipOrder(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.ShippingService/ShipOrder',
demo__pb2.ShipOrderRequest.SerializeToString,
demo__pb2.ShipOrderResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
class CurrencyServiceStub(object):
"""-----------------Currency service-----------------
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.GetSupportedCurrencies = channel.unary_unary(
'/hipstershop.CurrencyService/GetSupportedCurrencies',
request_serializer=demo__pb2.Empty.SerializeToString,
response_deserializer=demo__pb2.GetSupportedCurrenciesResponse.FromString,
)
self.Convert = channel.unary_unary(
'/hipstershop.CurrencyService/Convert',
request_serializer=demo__pb2.CurrencyConversionRequest.SerializeToString,
response_deserializer=demo__pb2.Money.FromString,
)
class CurrencyServiceServicer(object):
"""-----------------Currency service-----------------
"""
def GetSupportedCurrencies(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def Convert(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_CurrencyServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'GetSupportedCurrencies': grpc.unary_unary_rpc_method_handler(
servicer.GetSupportedCurrencies,
request_deserializer=demo__pb2.Empty.FromString,
response_serializer=demo__pb2.GetSupportedCurrenciesResponse.SerializeToString,
),
'Convert': grpc.unary_unary_rpc_method_handler(
servicer.Convert,
request_deserializer=demo__pb2.CurrencyConversionRequest.FromString,
response_serializer=demo__pb2.Money.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'hipstershop.CurrencyService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class CurrencyService(object):
"""-----------------Currency service-----------------
"""
@staticmethod
def GetSupportedCurrencies(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.CurrencyService/GetSupportedCurrencies',
demo__pb2.Empty.SerializeToString,
demo__pb2.GetSupportedCurrenciesResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def Convert(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.CurrencyService/Convert',
demo__pb2.CurrencyConversionRequest.SerializeToString,
demo__pb2.Money.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
class PaymentServiceStub(object):
"""-------------Payment service-----------------
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.Charge = channel.unary_unary(
'/hipstershop.PaymentService/Charge',
request_serializer=demo__pb2.ChargeRequest.SerializeToString,
response_deserializer=demo__pb2.ChargeResponse.FromString,
)
class PaymentServiceServicer(object):
"""-------------Payment service-----------------
"""
def Charge(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_PaymentServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'Charge': grpc.unary_unary_rpc_method_handler(
servicer.Charge,
request_deserializer=demo__pb2.ChargeRequest.FromString,
response_serializer=demo__pb2.ChargeResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'hipstershop.PaymentService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class PaymentService(object):
"""-------------Payment service-----------------
"""
@staticmethod
def Charge(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.PaymentService/Charge',
demo__pb2.ChargeRequest.SerializeToString,
demo__pb2.ChargeResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
class EmailServiceStub(object):
"""-------------Email service-----------------
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.SendOrderConfirmation = channel.unary_unary(
'/hipstershop.EmailService/SendOrderConfirmation',
request_serializer=demo__pb2.SendOrderConfirmationRequest.SerializeToString,
response_deserializer=demo__pb2.Empty.FromString,
)
class EmailServiceServicer(object):
"""-------------Email service-----------------
"""
def SendOrderConfirmation(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_EmailServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'SendOrderConfirmation': grpc.unary_unary_rpc_method_handler(
servicer.SendOrderConfirmation,
request_deserializer=demo__pb2.SendOrderConfirmationRequest.FromString,
response_serializer=demo__pb2.Empty.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'hipstershop.EmailService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class EmailService(object):
"""-------------Email service-----------------
"""
@staticmethod
def SendOrderConfirmation(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.EmailService/SendOrderConfirmation',
demo__pb2.SendOrderConfirmationRequest.SerializeToString,
demo__pb2.Empty.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
class CheckoutServiceStub(object):
"""-------------Checkout service-----------------
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.PlaceOrder = channel.unary_unary(
'/hipstershop.CheckoutService/PlaceOrder',
request_serializer=demo__pb2.PlaceOrderRequest.SerializeToString,
response_deserializer=demo__pb2.PlaceOrderResponse.FromString,
)
class CheckoutServiceServicer(object):
"""-------------Checkout service-----------------
"""
def PlaceOrder(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_CheckoutServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'PlaceOrder': grpc.unary_unary_rpc_method_handler(
servicer.PlaceOrder,
request_deserializer=demo__pb2.PlaceOrderRequest.FromString,
response_serializer=demo__pb2.PlaceOrderResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'hipstershop.CheckoutService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class CheckoutService(object):
"""-------------Checkout service-----------------
"""
@staticmethod
def PlaceOrder(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.CheckoutService/PlaceOrder',
demo__pb2.PlaceOrderRequest.SerializeToString,
demo__pb2.PlaceOrderResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
class AdServiceStub(object):
"""------------Ad service------------------
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.GetAds = channel.unary_unary(
'/hipstershop.AdService/GetAds',
request_serializer=demo__pb2.AdRequest.SerializeToString,
response_deserializer=demo__pb2.AdResponse.FromString,
)
class AdServiceServicer(object):
"""------------Ad service------------------
"""
def GetAds(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_AdServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'GetAds': grpc.unary_unary_rpc_method_handler(
servicer.GetAds,
request_deserializer=demo__pb2.AdRequest.FromString,
response_serializer=demo__pb2.AdResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'hipstershop.AdService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class AdService(object):
"""------------Ad service------------------
"""
@staticmethod
def GetAds(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/hipstershop.AdService/GetAds',
demo__pb2.AdRequest.SerializeToString,
demo__pb2.AdResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

View File

@@ -0,0 +1,39 @@
#!/usr/bin/python
#
# Copyright 2018 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.
import grpc
import demo_pb2
import demo_pb2_grpc
from logger import getJSONLogger
logger = getJSONLogger('emailservice-client')
def send_confirmation_email(email, order):
channel = grpc.insecure_channel('[::]:8080')
stub = demo_pb2_grpc.EmailServiceStub(channel)
try:
response = stub.SendOrderConfirmation(demo_pb2.SendOrderConfirmationRequest(
email = email,
order = order
))
logger.info('Request sent.')
except grpc.RpcError as err:
logger.error(err.details())
logger.error('{}, {}'.format(err.code().name, err.code().value))
if __name__ == '__main__':
logger.info('Client for email service.')

200
src/emailservice/email_server.py Executable file
View File

@@ -0,0 +1,200 @@
#!/usr/bin/python
#
# Copyright 2018 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.
from concurrent import futures
import argparse
import os
import sys
import time
import grpc
import traceback
from jinja2 import Environment, FileSystemLoader, select_autoescape, TemplateError
from google.api_core.exceptions import GoogleAPICallError
from google.auth.exceptions import DefaultCredentialsError
import demo_pb2
import demo_pb2_grpc
from grpc_health.v1 import health_pb2
from grpc_health.v1 import health_pb2_grpc
from opentelemetry import trace
from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# @TODO: Temporarily removed in https://github.com/GoogleCloudPlatform/microservices-demo/pull/3196
# import googlecloudprofiler
from logger import getJSONLogger
logger = getJSONLogger('emailservice-server')
# Loads confirmation email template from file
env = Environment(
loader=FileSystemLoader('templates'),
autoescape=select_autoescape(['html', 'xml'])
)
template = env.get_template('confirmation.html')
class BaseEmailService(demo_pb2_grpc.EmailServiceServicer):
def Check(self, request, context):
return health_pb2.HealthCheckResponse(
status=health_pb2.HealthCheckResponse.SERVING)
def Watch(self, request, context):
return health_pb2.HealthCheckResponse(
status=health_pb2.HealthCheckResponse.UNIMPLEMENTED)
class EmailService(BaseEmailService):
def __init__(self):
raise Exception('cloud mail client not implemented')
super().__init__()
@staticmethod
def send_email(client, email_address, content):
response = client.send_message(
sender = client.sender_path(project_id, region, sender_id),
envelope_from_authority = '',
header_from_authority = '',
envelope_from_address = from_address,
simple_message = {
"from": {
"address_spec": from_address,
},
"to": [{
"address_spec": email_address
}],
"subject": "Your Confirmation Email",
"html_body": content
}
)
logger.info("Message sent: {}".format(response.rfc822_message_id))
def SendOrderConfirmation(self, request, context):
email = request.email
order = request.order
try:
confirmation = template.render(order = order)
except TemplateError as err:
context.set_details("An error occurred when preparing the confirmation mail.")
logger.error(err.message)
context.set_code(grpc.StatusCode.INTERNAL)
return demo_pb2.Empty()
try:
EmailService.send_email(self.client, email, confirmation)
except GoogleAPICallError as err:
context.set_details("An error occurred when sending the email.")
print(err.message)
context.set_code(grpc.StatusCode.INTERNAL)
return demo_pb2.Empty()
return demo_pb2.Empty()
class DummyEmailService(BaseEmailService):
def SendOrderConfirmation(self, request, context):
logger.info('A request to send order confirmation email to {} has been received.'.format(request.email))
return demo_pb2.Empty()
class HealthCheck():
def Check(self, request, context):
return health_pb2.HealthCheckResponse(
status=health_pb2.HealthCheckResponse.SERVING)
def start(dummy_mode):
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10),)
service = None
if dummy_mode:
service = DummyEmailService()
else:
raise Exception('non-dummy mode not implemented yet')
demo_pb2_grpc.add_EmailServiceServicer_to_server(service, server)
health_pb2_grpc.add_HealthServicer_to_server(service, server)
port = os.environ.get('PORT', "8080")
logger.info("listening on port: "+port)
server.add_insecure_port('[::]:'+port)
server.start()
try:
while True:
time.sleep(3600)
except KeyboardInterrupt:
server.stop(0)
def initStackdriverProfiling():
project_id = None
try:
project_id = os.environ["GCP_PROJECT_ID"]
except KeyError:
# Environment variable not set
pass
# @TODO: Temporarily removed in https://github.com/GoogleCloudPlatform/microservices-demo/pull/3196
# for retry in range(1,4):
# try:
# if project_id:
# googlecloudprofiler.start(service='email_server', service_version='1.0.0', verbose=0, project_id=project_id)
# else:
# googlecloudprofiler.start(service='email_server', service_version='1.0.0', verbose=0)
# logger.info("Successfully started Stackdriver Profiler.")
# return
# except (BaseException) as exc:
# logger.info("Unable to start Stackdriver Profiler Python agent. " + str(exc))
# if (retry < 4):
# logger.info("Sleeping %d to retry initializing Stackdriver Profiler"%(retry*10))
# time.sleep (1)
# else:
# logger.warning("Could not initialize Stackdriver Profiler after retrying, giving up")
return
if __name__ == '__main__':
logger.info('starting the email service in dummy mode.')
# Profiler
try:
if "DISABLE_PROFILER" in os.environ:
raise KeyError()
else:
logger.info("Profiler enabled.")
initStackdriverProfiling()
except KeyError:
logger.info("Profiler disabled.")
# Tracing
try:
if os.environ["ENABLE_TRACING"] == "1":
otel_endpoint = os.getenv("COLLECTOR_SERVICE_ADDR", "localhost:4317")
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint = otel_endpoint,
insecure = True
)
)
)
grpc_server_instrumentor = GrpcInstrumentorServer()
grpc_server_instrumentor.instrument()
except (KeyError, DefaultCredentialsError):
logger.info("Tracing disabled.")
except Exception as e:
logger.warn(f"Exception on Cloud Trace setup: {traceback.format_exc()}, tracing disabled.")
start(dummy_mode = True)

21
src/emailservice/genproto.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash -eu
#
# Copyright 2018 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.
# [START gke_emailservice_genproto]
python -m grpc_tools.protoc -I../../protos --python_out=. --grpc_python_out=. ../../protos/demo.proto
# [END gke_emailservice_genproto]

41
src/emailservice/logger.py Executable file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/python
#
# Copyright 2018 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.
import logging
import sys
from pythonjsonlogger import jsonlogger
# TODO(yoshifumi) this class is duplicated since other Python services are
# not sharing the modules for logging.
class CustomJsonFormatter(jsonlogger.JsonFormatter):
def add_fields(self, log_record, record, message_dict):
super(CustomJsonFormatter, self).add_fields(log_record, record, message_dict)
if not log_record.get('timestamp'):
log_record['timestamp'] = record.created
if log_record.get('severity'):
log_record['severity'] = log_record['severity'].upper()
else:
log_record['severity'] = record.levelname
def getJSONLogger(name):
logger = logging.getLogger(name)
handler = logging.StreamHandler(sys.stdout)
formatter = CustomJsonFormatter('%(timestamp)s %(severity)s %(name)s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate = False
return logger

View File

@@ -0,0 +1,10 @@
google-api-core==2.28.1
grpcio-health-checking==1.76.0
grpcio==1.76.0
jinja2==3.1.6
python-json-logger==4.0.0
google-cloud-trace==1.17.0
requests==2.32.5
opentelemetry-distro==0.60b1
opentelemetry-instrumentation-grpc==0.60b1
opentelemetry-exporter-otlp-proto-grpc==1.39.1

View File

@@ -0,0 +1,120 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in -o requirements.txt
cachetools==5.3.2
# via google-auth
certifi==2024.7.4
# via requests
charset-normalizer==3.3.2
# via requests
google-api-core[grpc]==2.28.1
# via
# -r requirements.in
# google-cloud-trace
google-auth==2.23.4
# via
# google-api-core
# google-cloud-trace
google-cloud-trace==1.17.0
# via -r requirements.in
googleapis-common-protos==1.72.0
# via
# google-api-core
# grpcio-status
# opentelemetry-exporter-otlp-proto-grpc
grpcio==1.76.0
# via
# -r requirements.in
# google-api-core
# google-cloud-trace
# grpcio-health-checking
# grpcio-status
# opentelemetry-exporter-otlp-proto-grpc
grpcio-health-checking==1.76.0
# via -r requirements.in
grpcio-status==1.76.0
# via google-api-core
idna==3.7
# via requests
importlib-metadata==6.8.0
# via opentelemetry-api
jinja2==3.1.6
# via -r requirements.in
markupsafe==2.1.3
# via jinja2
opentelemetry-api==1.39.1
# via
# opentelemetry-distro
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-instrumentation
# opentelemetry-instrumentation-grpc
# opentelemetry-sdk
# opentelemetry-semantic-conventions
opentelemetry-distro==0.60b1
# via -r requirements.in
opentelemetry-exporter-otlp-proto-common==1.39.1
# via opentelemetry-exporter-otlp-proto-grpc
opentelemetry-exporter-otlp-proto-grpc==1.39.1
# via -r requirements.in
opentelemetry-instrumentation==0.60b1
# via
# opentelemetry-distro
# opentelemetry-instrumentation-grpc
opentelemetry-instrumentation-grpc==0.60b1
# via -r requirements.in
opentelemetry-proto==1.39.1
# via
# opentelemetry-exporter-otlp-proto-common
# opentelemetry-exporter-otlp-proto-grpc
opentelemetry-sdk==1.39.1
# via
# opentelemetry-distro
# opentelemetry-exporter-otlp-proto-grpc
opentelemetry-semantic-conventions==0.60b1
# via
# opentelemetry-instrumentation
# opentelemetry-instrumentation-grpc
# opentelemetry-sdk
packaging==25.0
# via opentelemetry-instrumentation
proto-plus==1.27.0
# via
# google-api-core
# google-cloud-trace
protobuf==6.33.5
# via
# google-api-core
# google-cloud-trace
# googleapis-common-protos
# grpcio-health-checking
# grpcio-status
# opentelemetry-proto
# proto-plus
pyasn1==0.5.0
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.3.0
# via google-auth
python-json-logger==4.0.0
# via -r requirements.in
requests==2.32.5
# via
# -r requirements.in
# google-api-core
rsa==4.9
# via google-auth
typing-extensions==4.15.0
# via
# grpcio
# opentelemetry-api
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-sdk
# opentelemetry-semantic-conventions
urllib3==2.6.3
# via requests
wrapt==1.16.0
# via
# opentelemetry-instrumentation
# opentelemetry-instrumentation-grpc
zipp==3.19.1
# via importlib-metadata

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<!--
Copyright 2020 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.
-->
<html>
<head>
<title>Your Order Confirmation</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
</head>
<style>
body{
font-family: 'DM Sans', sans-serif;
}
</style>
<body>
<h2>Your Order Confirmation</h2>
<p>Thanks for shopping with us!<p>
<h3>Order ID</h3>
<p>#{{ order.order_id }}</p>
<h3>Shipping</h3>
<p>#{{ order.shipping_tracking_id }}</p>
<p>{{ order.shipping_cost.units }}. {{ "%02d" | format(order.shipping_cost.nanos // 10000000) }} {{ order.shipping_cost.currency_code }}</p>
<p>{{ order.shipping_address.street_address_1 }}, {{order.shipping_address.street_address_2}}, {{order.shipping_address.city}}, {{order.shipping_address.country}} {{order.shipping_address.zip_code}}</p>
<h3>Items</h3>
<table style="width:100%">
<tr>
<th>Item No.</th>
<th>Quantity</th>
<th>Price</th>
</tr>
{% for item in order.items %}
<tr>
<td>#{{ item.item.product_id }}</td>
<td>{{ item.item.quantity }}</td>
<td>{{ item.cost.units }}.{{ "%02d" | format(item.cost.nanos // 10000000) }} {{ item.cost.currency_code }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>

View File

@@ -0,0 +1 @@
vendor/

0
src/frontend/.gitkeep Normal file
View File

44
src/frontend/Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
# Copyright 2020 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.
# Define a default value so it's not empty if the builder fails to provide it
ARG BUILDPLATFORM=linux/amd64
FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder
ARG TARGETOS=linux
ARG TARGETARCH=amd64
WORKDIR /src
# restore dependencies
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Skaffold passes in debug-oriented compiler flags
ARG SKAFFOLD_GO_GCFLAGS
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -ldflags="-s -w" -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /go/bin/frontend .
FROM gcr.io/distroless/static
WORKDIR /src
COPY --from=builder /go/bin/frontend /src/server
COPY ./templates ./templates
COPY ./static ./static
# Definition of this variable is used by 'skaffold debug' to identify a golang binary.
# Default behavior - a failure prints a stack trace for the current goroutine.
# See https://golang.org/pkg/runtime/
ENV GOTRACEBACK=single
EXPOSE 8080
ENTRYPOINT ["/src/server"]

5
src/frontend/README.md Normal file
View File

@@ -0,0 +1,5 @@
# frontend
Run the following command to restore dependencies to `vendor/` directory:
dep ensure --vendor-only

View File

@@ -0,0 +1,64 @@
package main
import (
"net/http"
"os"
"time"
"cloud.google.com/go/compute/metadata"
"github.com/sirupsen/logrus"
)
var deploymentDetailsMap map[string]string
var log *logrus.Logger
func init() {
initializeLogger()
// Use a goroutine to ensure loadDeploymentDetails()'s GCP API
// calls don't block non-GCP deployments. See issue #685.
go loadDeploymentDetails()
}
func initializeLogger() {
log = logrus.New()
log.Level = logrus.DebugLevel
log.Formatter = &logrus.JSONFormatter{
FieldMap: logrus.FieldMap{
logrus.FieldKeyTime: "timestamp",
logrus.FieldKeyLevel: "severity",
logrus.FieldKeyMsg: "message",
},
TimestampFormat: time.RFC3339Nano,
}
log.Out = os.Stdout
}
func loadDeploymentDetails() {
deploymentDetailsMap = make(map[string]string)
var metaServerClient = metadata.NewClient(&http.Client{})
podHostname, err := os.Hostname()
if err != nil {
log.Error("Failed to fetch the hostname for the Pod", err)
}
podCluster, err := metaServerClient.InstanceAttributeValue("cluster-name")
if err != nil {
log.Error("Failed to fetch the name of the cluster in which the pod is running", err)
}
podZone, err := metaServerClient.Zone()
if err != nil {
log.Error("Failed to fetch the Zone of the node where the pod is scheduled", err)
}
deploymentDetailsMap["HOSTNAME"] = podHostname
deploymentDetailsMap["CLUSTERNAME"] = podCluster
deploymentDetailsMap["ZONE"] = podZone
log.WithFields(logrus.Fields{
"cluster": podCluster,
"zone": podZone,
"hostname": podHostname,
}).Debug("Loaded deployment details")
}

25
src/frontend/genproto.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash -eu
#
# Copyright 2018 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.
# [START gke_frontend_genproto]
PATH=$PATH:$(go env GOPATH)/bin
protodir=../../protos
outdir=./genproto
protoc --proto_path=$protodir --go_out=./$outdir --go_opt=paths=source_relative --go-grpc_out=./$outdir --go-grpc_opt=paths=source_relative $protodir/demo.proto
# [END gke_frontend_genproto]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

58
src/frontend/go.mod Normal file
View File

@@ -0,0 +1,58 @@
module github.com/GoogleCloudPlatform/microservices-demo/src/frontend
go 1.25.0
toolchain go1.26.1
require (
cloud.google.com/go/compute/metadata v0.9.0
cloud.google.com/go/profiler v0.4.3
github.com/go-playground/validator/v10 v10.30.1
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.4
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0
go.opentelemetry.io/otel/sdk v1.42.0
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
)
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

146
src/frontend/go.sum Normal file
View File

@@ -0,0 +1,146 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/profiler v0.4.3 h1:IY3QNKlr8VbXwGWHcZbJQsMA/83ZTH6uAHf8jYyj7OI=
cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0=
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 h1:dDbsTLIK7EzwUq36kCSAsk0slouq/S0tWHeeGi97cD8=
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846/go.mod h1:PP0g88Dz3C7hRAfbQCQggeWAXjuqGsNPLE4s7jh0RGU=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

635
src/frontend/handlers.go Normal file
View File

@@ -0,0 +1,635 @@
// Copyright 2018 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 main
import (
"context"
"encoding/json"
"fmt"
"html/template"
"io"
"math/rand"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
"github.com/GoogleCloudPlatform/microservices-demo/src/frontend/money"
"github.com/GoogleCloudPlatform/microservices-demo/src/frontend/validator"
)
type platformDetails struct {
css string
provider string
}
var (
frontendMessage = strings.TrimSpace(os.Getenv("FRONTEND_MESSAGE"))
isCymbalBrand = "true" == strings.ToLower(os.Getenv("CYMBAL_BRANDING"))
assistantEnabled = "true" == strings.ToLower(os.Getenv("ENABLE_ASSISTANT"))
templates = template.Must(template.New("").
Funcs(template.FuncMap{
"renderMoney": renderMoney,
"renderCurrencyLogo": renderCurrencyLogo,
}).ParseGlob("templates/*.html"))
plat platformDetails
)
var validEnvs = []string{"local", "gcp", "azure", "aws", "onprem", "alibaba"}
func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
log.WithField("currency", currentCurrency(r)).Info("home")
currencies, err := fe.getCurrencies(r.Context())
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
return
}
products, err := fe.getProducts(r.Context())
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve products"), http.StatusInternalServerError)
return
}
cart, err := fe.getCart(r.Context(), sessionID(r))
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
return
}
type productView struct {
Item *pb.Product
Price *pb.Money
}
ps := make([]productView, len(products))
for i, p := range products {
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
if err != nil {
renderHTTPError(log, r, w, errors.Wrapf(err, "failed to do currency conversion for product %s", p.GetId()), http.StatusInternalServerError)
return
}
ps[i] = productView{p, price}
}
// Set ENV_PLATFORM (default to local if not set; use env var if set; otherwise detect GCP, which overrides env)_
var env = os.Getenv("ENV_PLATFORM")
// Only override from env variable if set + valid env
if env == "" || stringinSlice(validEnvs, env) == false {
fmt.Println("env platform is either empty or invalid")
env = "local"
}
// Autodetect GCP
addrs, err := net.LookupHost("metadata.google.internal.")
if err == nil && len(addrs) >= 0 {
log.Debugf("Detected Google metadata server: %v, setting ENV_PLATFORM to GCP.", addrs)
env = "gcp"
}
log.Debugf("ENV_PLATFORM is: %s", env)
plat = platformDetails{}
plat.setPlatformDetails(strings.ToLower(env))
if err := templates.ExecuteTemplate(w, "home", injectCommonTemplateData(r, map[string]interface{}{
"show_currency": true,
"currencies": currencies,
"products": ps,
"cart_size": cartSize(cart),
"banner_color": os.Getenv("BANNER_COLOR"), // illustrates canary deployments
"ad": fe.chooseAd(r.Context(), []string{}, log),
})); err != nil {
log.Error(err)
}
}
func (plat *platformDetails) setPlatformDetails(env string) {
if env == "aws" {
plat.provider = "AWS"
plat.css = "aws-platform"
} else if env == "onprem" {
plat.provider = "On-Premises"
plat.css = "onprem-platform"
} else if env == "azure" {
plat.provider = "Azure"
plat.css = "azure-platform"
} else if env == "gcp" {
plat.provider = "Google Cloud"
plat.css = "gcp-platform"
} else if env == "alibaba" {
plat.provider = "Alibaba Cloud"
plat.css = "alibaba-platform"
} else {
plat.provider = "local"
plat.css = "local"
}
}
func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
id := mux.Vars(r)["id"]
if id == "" {
renderHTTPError(log, r, w, errors.New("product id not specified"), http.StatusBadRequest)
return
}
log.WithField("id", id).WithField("currency", currentCurrency(r)).
Debug("serving product page")
p, err := fe.getProduct(r.Context(), id)
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
return
}
currencies, err := fe.getCurrencies(r.Context())
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
return
}
cart, err := fe.getCart(r.Context(), sessionID(r))
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
return
}
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to convert currency"), http.StatusInternalServerError)
return
}
// ignores the error retrieving recommendations since it is not critical
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), []string{id})
if err != nil {
log.WithField("error", err).Warn("failed to get product recommendations")
}
product := struct {
Item *pb.Product
Price *pb.Money
}{p, price}
// Fetch packaging info (weight/dimensions) of the product
// The packaging service is an optional microservice you can run as part of a Google Cloud demo.
var packagingInfo *PackagingInfo = nil
if isPackagingServiceConfigured() {
packagingInfo, err = httpGetPackagingInfo(id)
if err != nil {
fmt.Println("Failed to obtain product's packaging info:", err)
}
}
if err := templates.ExecuteTemplate(w, "product", injectCommonTemplateData(r, map[string]interface{}{
"ad": fe.chooseAd(r.Context(), p.Categories, log),
"show_currency": true,
"currencies": currencies,
"product": product,
"recommendations": recommendations,
"cart_size": cartSize(cart),
"packagingInfo": packagingInfo,
})); err != nil {
log.Println(err)
}
}
func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32)
productID := r.FormValue("product_id")
payload := validator.AddToCartPayload{
Quantity: quantity,
ProductID: productID,
}
if err := payload.Validate(); err != nil {
renderHTTPError(log, r, w, validator.ValidationErrorResponse(err), http.StatusUnprocessableEntity)
return
}
log.WithField("product", payload.ProductID).WithField("quantity", payload.Quantity).Debug("adding to cart")
p, err := fe.getProduct(r.Context(), payload.ProductID)
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
return
}
if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(payload.Quantity)); err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to add to cart"), http.StatusInternalServerError)
return
}
w.Header().Set("location", baseUrl + "/cart")
w.WriteHeader(http.StatusFound)
}
func (fe *frontendServer) emptyCartHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
log.Debug("emptying cart")
if err := fe.emptyCart(r.Context(), sessionID(r)); err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to empty cart"), http.StatusInternalServerError)
return
}
w.Header().Set("location", baseUrl + "/")
w.WriteHeader(http.StatusFound)
}
func (fe *frontendServer) viewCartHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
log.Debug("view user cart")
currencies, err := fe.getCurrencies(r.Context())
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
return
}
cart, err := fe.getCart(r.Context(), sessionID(r))
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve cart"), http.StatusInternalServerError)
return
}
// ignores the error retrieving recommendations since it is not critical
recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), cartIDs(cart))
if err != nil {
log.WithField("error", err).Warn("failed to get product recommendations")
}
shippingCost, err := fe.getShippingQuote(r.Context(), cart, currentCurrency(r))
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to get shipping quote"), http.StatusInternalServerError)
return
}
type cartItemView struct {
Item *pb.Product
Quantity int32
Price *pb.Money
}
items := make([]cartItemView, len(cart))
totalPrice := pb.Money{CurrencyCode: currentCurrency(r)}
for i, item := range cart {
p, err := fe.getProduct(r.Context(), item.GetProductId())
if err != nil {
renderHTTPError(log, r, w, errors.Wrapf(err, "could not retrieve product #%s", item.GetProductId()), http.StatusInternalServerError)
return
}
price, err := fe.convertCurrency(r.Context(), p.GetPriceUsd(), currentCurrency(r))
if err != nil {
renderHTTPError(log, r, w, errors.Wrapf(err, "could not convert currency for product #%s", item.GetProductId()), http.StatusInternalServerError)
return
}
multPrice := money.MultiplySlow(*price, uint32(item.GetQuantity()))
items[i] = cartItemView{
Item: p,
Quantity: item.GetQuantity(),
Price: &multPrice}
totalPrice = money.Must(money.Sum(totalPrice, multPrice))
}
totalPrice = money.Must(money.Sum(totalPrice, *shippingCost))
year := time.Now().Year()
if err := templates.ExecuteTemplate(w, "cart", injectCommonTemplateData(r, map[string]interface{}{
"currencies": currencies,
"recommendations": recommendations,
"cart_size": cartSize(cart),
"shipping_cost": shippingCost,
"show_currency": true,
"total_cost": totalPrice,
"items": items,
"expiration_years": []int{year, year + 1, year + 2, year + 3, year + 4},
})); err != nil {
log.Println(err)
}
}
func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
log.Debug("placing order")
var (
email = r.FormValue("email")
streetAddress = r.FormValue("street_address")
zipCode, _ = strconv.ParseInt(r.FormValue("zip_code"), 10, 32)
city = r.FormValue("city")
state = r.FormValue("state")
country = r.FormValue("country")
ccNumber = r.FormValue("credit_card_number")
ccMonth, _ = strconv.ParseInt(r.FormValue("credit_card_expiration_month"), 10, 32)
ccYear, _ = strconv.ParseInt(r.FormValue("credit_card_expiration_year"), 10, 32)
ccCVV, _ = strconv.ParseInt(r.FormValue("credit_card_cvv"), 10, 32)
)
payload := validator.PlaceOrderPayload{
Email: email,
StreetAddress: streetAddress,
ZipCode: zipCode,
City: city,
State: state,
Country: country,
CcNumber: ccNumber,
CcMonth: ccMonth,
CcYear: ccYear,
CcCVV: ccCVV,
}
if err := payload.Validate(); err != nil {
renderHTTPError(log, r, w, validator.ValidationErrorResponse(err), http.StatusUnprocessableEntity)
return
}
order, err := pb.NewCheckoutServiceClient(fe.checkoutSvcConn).
PlaceOrder(r.Context(), &pb.PlaceOrderRequest{
Email: payload.Email,
CreditCard: &pb.CreditCardInfo{
CreditCardNumber: payload.CcNumber,
CreditCardExpirationMonth: int32(payload.CcMonth),
CreditCardExpirationYear: int32(payload.CcYear),
CreditCardCvv: int32(payload.CcCVV)},
UserId: sessionID(r),
UserCurrency: currentCurrency(r),
Address: &pb.Address{
StreetAddress: payload.StreetAddress,
City: payload.City,
State: payload.State,
ZipCode: int32(payload.ZipCode),
Country: payload.Country},
})
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to complete the order"), http.StatusInternalServerError)
return
}
log.WithField("order", order.GetOrder().GetOrderId()).Info("order placed")
order.GetOrder().GetItems()
recommendations, _ := fe.getRecommendations(r.Context(), sessionID(r), nil)
totalPaid := *order.GetOrder().GetShippingCost()
for _, v := range order.GetOrder().GetItems() {
multPrice := money.MultiplySlow(*v.GetCost(), uint32(v.GetItem().GetQuantity()))
totalPaid = money.Must(money.Sum(totalPaid, multPrice))
}
currencies, err := fe.getCurrencies(r.Context())
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
return
}
if err := templates.ExecuteTemplate(w, "order", injectCommonTemplateData(r, map[string]interface{}{
"show_currency": false,
"currencies": currencies,
"order": order.GetOrder(),
"total_paid": &totalPaid,
"recommendations": recommendations,
})); err != nil {
log.Println(err)
}
}
func (fe *frontendServer) assistantHandler(w http.ResponseWriter, r *http.Request) {
currencies, err := fe.getCurrencies(r.Context())
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
return
}
if err := templates.ExecuteTemplate(w, "assistant", injectCommonTemplateData(r, map[string]interface{}{
"show_currency": false,
"currencies": currencies,
})); err != nil {
log.Println(err)
}
}
func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
log.Debug("logging out")
for _, c := range r.Cookies() {
c.Expires = time.Now().Add(-time.Hour * 24 * 365)
c.MaxAge = -1
http.SetCookie(w, c)
}
w.Header().Set("Location", baseUrl + "/")
w.WriteHeader(http.StatusFound)
}
func (fe *frontendServer) getProductByID(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["ids"]
if id == "" {
return
}
p, err := fe.getProduct(r.Context(), id)
if err != nil {
return
}
jsonData, err := json.Marshal(p)
if err != nil {
fmt.Println(err)
return
}
w.Write(jsonData)
w.WriteHeader(http.StatusOK)
}
func (fe *frontendServer) chatBotHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
type Response struct {
Message string `json:"message"`
}
type LLMResponse struct {
Content string `json:"content"`
Details map[string]any `json:"details"`
}
var response LLMResponse
url := "http://" + fe.shoppingAssistantSvcAddr
req, err := http.NewRequest(http.MethodPost, url, r.Body)
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to create request"), http.StatusInternalServerError)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to send request"), http.StatusInternalServerError)
return
}
body, err := io.ReadAll(res.Body)
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to read response"), http.StatusInternalServerError)
return
}
fmt.Printf("%+v\n", body)
fmt.Printf("%+v\n", res)
err = json.Unmarshal(body, &response)
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to unmarshal body"), http.StatusInternalServerError)
return
}
// respond with the same message
json.NewEncoder(w).Encode(Response{Message: response.Content})
w.WriteHeader(http.StatusOK)
}
func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
cur := r.FormValue("currency_code")
payload := validator.SetCurrencyPayload{Currency: cur}
if err := payload.Validate(); err != nil {
renderHTTPError(log, r, w, validator.ValidationErrorResponse(err), http.StatusUnprocessableEntity)
return
}
log.WithField("curr.new", payload.Currency).WithField("curr.old", currentCurrency(r)).
Debug("setting currency")
if payload.Currency != "" {
http.SetCookie(w, &http.Cookie{
Name: cookieCurrency,
Value: payload.Currency,
MaxAge: cookieMaxAge,
})
}
referer := r.Header.Get("referer")
if referer == "" {
referer = baseUrl + "/"
}
w.Header().Set("Location", referer)
w.WriteHeader(http.StatusFound)
}
// chooseAd queries for advertisements available and randomly chooses one, if
// available. It ignores the error retrieving the ad since it is not critical.
func (fe *frontendServer) chooseAd(ctx context.Context, ctxKeys []string, log logrus.FieldLogger) *pb.Ad {
ads, err := fe.getAd(ctx, ctxKeys)
if err != nil {
log.WithField("error", err).Warn("failed to retrieve ads")
return nil
}
return ads[rand.Intn(len(ads))]
}
func renderHTTPError(log logrus.FieldLogger, r *http.Request, w http.ResponseWriter, err error, code int) {
log.WithField("error", err).Error("request error")
errMsg := fmt.Sprintf("%+v", err)
w.WriteHeader(code)
if templateErr := templates.ExecuteTemplate(w, "error", injectCommonTemplateData(r, map[string]interface{}{
"error": errMsg,
"status_code": code,
"status": http.StatusText(code),
})); templateErr != nil {
log.Println(templateErr)
}
}
func injectCommonTemplateData(r *http.Request, payload map[string]interface{}) map[string]interface{} {
data := map[string]interface{}{
"session_id": sessionID(r),
"request_id": r.Context().Value(ctxKeyRequestID{}),
"user_currency": currentCurrency(r),
"platform_css": plat.css,
"platform_name": plat.provider,
"is_cymbal_brand": isCymbalBrand,
"assistant_enabled": assistantEnabled,
"deploymentDetails": deploymentDetailsMap,
"frontendMessage": frontendMessage,
"currentYear": time.Now().Year(),
"baseUrl": baseUrl,
}
for k, v := range payload {
data[k] = v
}
return data
}
func currentCurrency(r *http.Request) string {
c, _ := r.Cookie(cookieCurrency)
if c != nil {
return c.Value
}
return defaultCurrency
}
func sessionID(r *http.Request) string {
v := r.Context().Value(ctxKeySessionID{})
if v != nil {
return v.(string)
}
return ""
}
func cartIDs(c []*pb.CartItem) []string {
out := make([]string, len(c))
for i, v := range c {
out[i] = v.GetProductId()
}
return out
}
// get total # of items in cart
func cartSize(c []*pb.CartItem) int {
cartSize := 0
for _, item := range c {
cartSize += int(item.GetQuantity())
}
return cartSize
}
func renderMoney(money pb.Money) string {
currencyLogo := renderCurrencyLogo(money.GetCurrencyCode())
return fmt.Sprintf("%s%d.%02d", currencyLogo, money.GetUnits(), money.GetNanos()/10000000)
}
func renderCurrencyLogo(currencyCode string) string {
logos := map[string]string{
"USD": "$",
"CAD": "$",
"JPY": "¥",
"EUR": "€",
"TRY": "₺",
"GBP": "£",
}
logo := "$" //default
if val, ok := logos[currencyCode]; ok {
logo = val
}
return logo
}
func stringinSlice(slice []string, val string) bool {
for _, item := range slice {
if item == val {
return true
}
}
return false
}

235
src/frontend/main.go Normal file
View File

@@ -0,0 +1,235 @@
// Copyright 2018 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 main
import (
"context"
"fmt"
"net/http"
"os"
"time"
"cloud.google.com/go/profiler"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const (
port = "8080"
defaultCurrency = "USD"
cookieMaxAge = 60 * 60 * 48
cookiePrefix = "shop_"
cookieSessionID = cookiePrefix + "session-id"
cookieCurrency = cookiePrefix + "currency"
)
var (
whitelistedCurrencies = map[string]bool{
"USD": true,
"EUR": true,
"CAD": true,
"JPY": true,
"GBP": true,
"TRY": true,
}
baseUrl = ""
)
type ctxKeySessionID struct{}
type frontendServer struct {
productCatalogSvcAddr string
productCatalogSvcConn *grpc.ClientConn
currencySvcAddr string
currencySvcConn *grpc.ClientConn
cartSvcAddr string
cartSvcConn *grpc.ClientConn
recommendationSvcAddr string
recommendationSvcConn *grpc.ClientConn
checkoutSvcAddr string
checkoutSvcConn *grpc.ClientConn
shippingSvcAddr string
shippingSvcConn *grpc.ClientConn
adSvcAddr string
adSvcConn *grpc.ClientConn
collectorAddr string
collectorConn *grpc.ClientConn
shoppingAssistantSvcAddr string
}
func main() {
ctx := context.Background()
log := logrus.New()
log.Level = logrus.DebugLevel
log.Formatter = &logrus.JSONFormatter{
FieldMap: logrus.FieldMap{
logrus.FieldKeyTime: "timestamp",
logrus.FieldKeyLevel: "severity",
logrus.FieldKeyMsg: "message",
},
TimestampFormat: time.RFC3339Nano,
}
log.Out = os.Stdout
svc := new(frontendServer)
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, propagation.Baggage{}))
baseUrl = os.Getenv("BASE_URL")
if os.Getenv("ENABLE_TRACING") == "1" {
log.Info("Tracing enabled.")
initTracing(log, ctx, svc)
} else {
log.Info("Tracing disabled.")
}
if os.Getenv("ENABLE_PROFILER") == "1" {
log.Info("Profiling enabled.")
go initProfiling(log, "frontend", "1.0.0")
} else {
log.Info("Profiling disabled.")
}
srvPort := port
if os.Getenv("PORT") != "" {
srvPort = os.Getenv("PORT")
}
addr := os.Getenv("LISTEN_ADDR")
mustMapEnv(&svc.productCatalogSvcAddr, "PRODUCT_CATALOG_SERVICE_ADDR")
mustMapEnv(&svc.currencySvcAddr, "CURRENCY_SERVICE_ADDR")
mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR")
mustMapEnv(&svc.recommendationSvcAddr, "RECOMMENDATION_SERVICE_ADDR")
mustMapEnv(&svc.checkoutSvcAddr, "CHECKOUT_SERVICE_ADDR")
mustMapEnv(&svc.shippingSvcAddr, "SHIPPING_SERVICE_ADDR")
mustMapEnv(&svc.adSvcAddr, "AD_SERVICE_ADDR")
mustMapEnv(&svc.shoppingAssistantSvcAddr, "SHOPPING_ASSISTANT_SERVICE_ADDR")
mustConnGRPC(ctx, &svc.currencySvcConn, svc.currencySvcAddr)
mustConnGRPC(ctx, &svc.productCatalogSvcConn, svc.productCatalogSvcAddr)
mustConnGRPC(ctx, &svc.cartSvcConn, svc.cartSvcAddr)
mustConnGRPC(ctx, &svc.recommendationSvcConn, svc.recommendationSvcAddr)
mustConnGRPC(ctx, &svc.shippingSvcConn, svc.shippingSvcAddr)
mustConnGRPC(ctx, &svc.checkoutSvcConn, svc.checkoutSvcAddr)
mustConnGRPC(ctx, &svc.adSvcConn, svc.adSvcAddr)
r := mux.NewRouter()
r.HandleFunc(baseUrl+"/", svc.homeHandler).Methods(http.MethodGet, http.MethodHead)
r.HandleFunc(baseUrl+"/product/{id}", svc.productHandler).Methods(http.MethodGet, http.MethodHead)
r.HandleFunc(baseUrl+"/cart", svc.viewCartHandler).Methods(http.MethodGet, http.MethodHead)
r.HandleFunc(baseUrl+"/cart", svc.addToCartHandler).Methods(http.MethodPost)
r.HandleFunc(baseUrl+"/cart/empty", svc.emptyCartHandler).Methods(http.MethodPost)
r.HandleFunc(baseUrl+"/setCurrency", svc.setCurrencyHandler).Methods(http.MethodPost)
r.HandleFunc(baseUrl+"/logout", svc.logoutHandler).Methods(http.MethodGet)
r.HandleFunc(baseUrl+"/cart/checkout", svc.placeOrderHandler).Methods(http.MethodPost)
r.HandleFunc(baseUrl+"/assistant", svc.assistantHandler).Methods(http.MethodGet)
r.PathPrefix(baseUrl + "/static/").Handler(http.StripPrefix(baseUrl+"/static/", http.FileServer(http.Dir("./static/"))))
r.HandleFunc(baseUrl+"/robots.txt", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "User-agent: *\nDisallow: /") })
r.HandleFunc(baseUrl+"/_healthz", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "ok") })
r.HandleFunc(baseUrl+"/product-meta/{ids}", svc.getProductByID).Methods(http.MethodGet)
r.HandleFunc(baseUrl+"/bot", svc.chatBotHandler).Methods(http.MethodPost)
var handler http.Handler = r
handler = &logHandler{log: log, next: handler} // add logging
handler = ensureSessionID(handler) // add session ID
handler = otelhttp.NewHandler(handler, "frontend") // add OTel tracing
log.Infof("starting server on %s:%s", addr, srvPort)
log.Fatal(http.ListenAndServe(addr+":"+srvPort, handler))
}
func initStats(log logrus.FieldLogger) {
// TODO(arbrown) Implement OpenTelemtry stats
}
func initTracing(log logrus.FieldLogger, ctx context.Context, svc *frontendServer) (*sdktrace.TracerProvider, error) {
mustMapEnv(&svc.collectorAddr, "COLLECTOR_SERVICE_ADDR")
mustConnGRPC(ctx, &svc.collectorConn, svc.collectorAddr)
exporter, err := otlptracegrpc.New(
ctx,
otlptracegrpc.WithGRPCConn(svc.collectorConn))
if err != nil {
log.Warnf("warn: Failed to create trace exporter: %v", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
return tp, err
}
func initProfiling(log logrus.FieldLogger, service, version string) {
// TODO(ahmetb) this method is duplicated in other microservices using Go
// since they are not sharing packages.
for i := 1; i <= 3; i++ {
log = log.WithField("retry", i)
if err := profiler.Start(profiler.Config{
Service: service,
ServiceVersion: version,
// ProjectID must be set if not running on GCP.
// ProjectID: "my-project",
}); err != nil {
log.Warnf("warn: failed to start profiler: %+v", err)
} else {
log.Info("started Stackdriver profiler")
return
}
d := time.Second * 10 * time.Duration(i)
log.Debugf("sleeping %v to retry initializing Stackdriver profiler", d)
time.Sleep(d)
}
log.Warn("warning: could not initialize Stackdriver profiler after retrying, giving up")
}
func mustMapEnv(target *string, envKey string) {
v := os.Getenv(envKey)
if v == "" {
panic(fmt.Sprintf("environment variable %q not set", envKey))
}
*target = v
}
func mustConnGRPC(ctx context.Context, conn **grpc.ClientConn, addr string) {
var err error
_, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
*conn, err = grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
if err != nil {
panic(errors.Wrapf(err, "grpc: failed to connect %s", addr))
}
}

111
src/frontend/middleware.go Normal file
View File

@@ -0,0 +1,111 @@
// Copyright 2018 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 main
import (
"context"
"net/http"
"time"
"os"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
type ctxKeyLog struct{}
type ctxKeyRequestID struct{}
type logHandler struct {
log *logrus.Logger
next http.Handler
}
type responseRecorder struct {
b int
status int
w http.ResponseWriter
}
func (r *responseRecorder) Header() http.Header { return r.w.Header() }
func (r *responseRecorder) Write(p []byte) (int, error) {
if r.status == 0 {
r.status = http.StatusOK
}
n, err := r.w.Write(p)
r.b += n
return n, err
}
func (r *responseRecorder) WriteHeader(statusCode int) {
r.status = statusCode
r.w.WriteHeader(statusCode)
}
func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID, _ := uuid.NewRandom()
ctx = context.WithValue(ctx, ctxKeyRequestID{}, requestID.String())
start := time.Now()
rr := &responseRecorder{w: w}
log := lh.log.WithFields(logrus.Fields{
"http.req.path": r.URL.Path,
"http.req.method": r.Method,
"http.req.id": requestID.String(),
})
if v, ok := r.Context().Value(ctxKeySessionID{}).(string); ok {
log = log.WithField("session", v)
}
log.Debug("request started")
defer func() {
log.WithFields(logrus.Fields{
"http.resp.took_ms": int64(time.Since(start) / time.Millisecond),
"http.resp.status": rr.status,
"http.resp.bytes": rr.b}).Debugf("request complete")
}()
ctx = context.WithValue(ctx, ctxKeyLog{}, log)
r = r.WithContext(ctx)
lh.next.ServeHTTP(rr, r)
}
func ensureSessionID(next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var sessionID string
c, err := r.Cookie(cookieSessionID)
if err == http.ErrNoCookie {
if os.Getenv("ENABLE_SINGLE_SHARED_SESSION") == "true" {
// Hard coded user id, shared across sessions
sessionID = "12345678-1234-1234-1234-123456789123"
} else {
u, _ := uuid.NewRandom()
sessionID = u.String()
}
http.SetCookie(w, &http.Cookie{
Name: cookieSessionID,
Value: sessionID,
MaxAge: cookieMaxAge,
})
} else if err != nil {
return
} else {
sessionID = c.Value
}
ctx := context.WithValue(r.Context(), ctxKeySessionID{}, sessionID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}
}

132
src/frontend/money/money.go Normal file
View File

@@ -0,0 +1,132 @@
// Copyright 2018 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 money
import (
"errors"
pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
)
const (
nanosMin = -999999999
nanosMax = +999999999
nanosMod = 1000000000
)
var (
ErrInvalidValue = errors.New("one of the specified money values is invalid")
ErrMismatchingCurrency = errors.New("mismatching currency codes")
)
// IsValid checks if specified value has a valid units/nanos signs and ranges.
func IsValid(m pb.Money) bool {
return signMatches(m) && validNanos(m.GetNanos())
}
func signMatches(m pb.Money) bool {
return m.GetNanos() == 0 || m.GetUnits() == 0 || (m.GetNanos() < 0) == (m.GetUnits() < 0)
}
func validNanos(nanos int32) bool { return nanosMin <= nanos && nanos <= nanosMax }
// IsZero returns true if the specified money value is equal to zero.
func IsZero(m pb.Money) bool { return m.GetUnits() == 0 && m.GetNanos() == 0 }
// IsPositive returns true if the specified money value is valid and is
// positive.
func IsPositive(m pb.Money) bool {
return IsValid(m) && m.GetUnits() > 0 || (m.GetUnits() == 0 && m.GetNanos() > 0)
}
// IsNegative returns true if the specified money value is valid and is
// negative.
func IsNegative(m pb.Money) bool {
return IsValid(m) && m.GetUnits() < 0 || (m.GetUnits() == 0 && m.GetNanos() < 0)
}
// AreSameCurrency returns true if values l and r have a currency code and
// they are the same values.
func AreSameCurrency(l, r pb.Money) bool {
return l.GetCurrencyCode() == r.GetCurrencyCode() && l.GetCurrencyCode() != ""
}
// AreEquals returns true if values l and r are the equal, including the
// currency. This does not check validity of the provided values.
func AreEquals(l, r pb.Money) bool {
return l.GetCurrencyCode() == r.GetCurrencyCode() &&
l.GetUnits() == r.GetUnits() && l.GetNanos() == r.GetNanos()
}
// Negate returns the same amount with the sign negated.
func Negate(m pb.Money) pb.Money {
return pb.Money{
Units: -m.GetUnits(),
Nanos: -m.GetNanos(),
CurrencyCode: m.GetCurrencyCode()}
}
// Must panics if the given error is not nil. This can be used with other
// functions like: "m := Must(Sum(a,b))".
func Must(v pb.Money, err error) pb.Money {
if err != nil {
panic(err)
}
return v
}
// Sum adds two values. Returns an error if one of the values are invalid or
// currency codes are not matching (unless currency code is unspecified for
// both).
func Sum(l, r pb.Money) (pb.Money, error) {
if !IsValid(l) || !IsValid(r) {
return pb.Money{}, ErrInvalidValue
} else if l.GetCurrencyCode() != r.GetCurrencyCode() {
return pb.Money{}, ErrMismatchingCurrency
}
units := l.GetUnits() + r.GetUnits()
nanos := l.GetNanos() + r.GetNanos()
if (units == 0 && nanos == 0) || (units > 0 && nanos >= 0) || (units < 0 && nanos <= 0) {
// same sign <units, nanos>
units += int64(nanos / nanosMod)
nanos = nanos % nanosMod
} else {
// different sign. nanos guaranteed to not to go over the limit
if units > 0 {
units--
nanos += nanosMod
} else {
units++
nanos -= nanosMod
}
}
return pb.Money{
Units: units,
Nanos: nanos,
CurrencyCode: l.GetCurrencyCode()}, nil
}
// MultiplySlow is a slow multiplication operation done through adding the value
// to itself n-1 times.
func MultiplySlow(m pb.Money, n uint32) pb.Money {
out := m
for n > 1 {
out = Must(Sum(out, m))
n--
}
return out
}

View File

@@ -0,0 +1,245 @@
// Copyright 2018 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 money
import (
"fmt"
"reflect"
"testing"
pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
)
func mmc(u int64, n int32, c string) pb.Money { return pb.Money{Units: u, Nanos: n, CurrencyCode: c} }
func mm(u int64, n int32) pb.Money { return mmc(u, n, "") }
func TestIsValid(t *testing.T) {
tests := []struct {
name string
in pb.Money
want bool
}{
{"valid -/-", mm(-981273891273, -999999999), true},
{"invalid -/+", mm(-981273891273, +999999999), false},
{"valid +/+", mm(981273891273, 999999999), true},
{"invalid +/-", mm(981273891273, -999999999), false},
{"invalid +/+overflow", mm(3, 1000000000), false},
{"invalid +/-overflow", mm(3, -1000000000), false},
{"invalid -/+overflow", mm(-3, 1000000000), false},
{"invalid -/-overflow", mm(-3, -1000000000), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValid(tt.in); got != tt.want {
t.Errorf("IsValid(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestIsZero(t *testing.T) {
tests := []struct {
name string
in pb.Money
want bool
}{
{"zero", mm(0, 0), true},
{"not-zero (-/+)", mm(-1, +1), false},
{"not-zero (-/-)", mm(-1, -1), false},
{"not-zero (+/+)", mm(+1, +1), false},
{"not-zero (+/-)", mm(+1, -1), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsZero(tt.in); got != tt.want {
t.Errorf("IsZero(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestIsPositive(t *testing.T) {
tests := []struct {
name string
in pb.Money
want bool
}{
{"zero", mm(0, 0), false},
{"positive (+/+)", mm(+1, +1), true},
{"invalid (-/+)", mm(-1, +1), false},
{"negative (-/-)", mm(-1, -1), false},
{"invalid (+/-)", mm(+1, -1), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsPositive(tt.in); got != tt.want {
t.Errorf("IsPositive(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestIsNegative(t *testing.T) {
tests := []struct {
name string
in pb.Money
want bool
}{
{"zero", mm(0, 0), false},
{"positive (+/+)", mm(+1, +1), false},
{"invalid (-/+)", mm(-1, +1), false},
{"negative (-/-)", mm(-1, -1), true},
{"invalid (+/-)", mm(+1, -1), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsNegative(tt.in); got != tt.want {
t.Errorf("IsNegative(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestAreSameCurrency(t *testing.T) {
type args struct {
l pb.Money
r pb.Money
}
tests := []struct {
name string
args args
want bool
}{
{"both empty currency", args{mmc(1, 0, ""), mmc(2, 0, "")}, false},
{"left empty currency", args{mmc(1, 0, ""), mmc(2, 0, "USD")}, false},
{"right empty currency", args{mmc(1, 0, "USD"), mmc(2, 0, "")}, false},
{"mismatching", args{mmc(1, 0, "USD"), mmc(2, 0, "CAD")}, false},
{"matching", args{mmc(1, 0, "USD"), mmc(2, 0, "USD")}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := AreSameCurrency(tt.args.l, tt.args.r); got != tt.want {
t.Errorf("AreSameCurrency([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want)
}
})
}
}
func TestAreEquals(t *testing.T) {
type args struct {
l pb.Money
r pb.Money
}
tests := []struct {
name string
args args
want bool
}{
{"equals", args{mmc(1, 2, "USD"), mmc(1, 2, "USD")}, true},
{"mismatching currency", args{mmc(1, 2, "USD"), mmc(1, 2, "CAD")}, false},
{"mismatching units", args{mmc(10, 20, "USD"), mmc(1, 20, "USD")}, false},
{"mismatching nanos", args{mmc(1, 2, "USD"), mmc(1, 20, "USD")}, false},
{"negated", args{mmc(1, 2, "USD"), mmc(-1, -2, "USD")}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := AreEquals(tt.args.l, tt.args.r); got != tt.want {
t.Errorf("AreEquals([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want)
}
})
}
}
func TestNegate(t *testing.T) {
tests := []struct {
name string
in pb.Money
want pb.Money
}{
{"zero", mm(0, 0), mm(0, 0)},
{"negative", mm(-1, -200), mm(1, 200)},
{"positive", mm(1, 200), mm(-1, -200)},
{"carries currency code", mmc(0, 0, "XXX"), mmc(0, 0, "XXX")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Negate(tt.in); !AreEquals(got, tt.want) {
t.Errorf("Negate([%v]) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestMust_pass(t *testing.T) {
v := Must(mm(2, 3), nil)
if !AreEquals(v, mm(2, 3)) {
t.Errorf("returned the wrong value: %v", v)
}
}
func TestMust_panic(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Logf("panic captured: %v", r)
}
}()
Must(mm(2, 3), fmt.Errorf("some error"))
t.Fatal("this should not have executed due to the panic above")
}
func TestSum(t *testing.T) {
type args struct {
l pb.Money
r pb.Money
}
tests := []struct {
name string
args args
want pb.Money
wantErr error
}{
{"0+0=0", args{mm(0, 0), mm(0, 0)}, mm(0, 0), nil},
{"Error: currency code on left", args{mmc(0, 0, "XXX"), mm(0, 0)}, mm(0, 0), ErrMismatchingCurrency},
{"Error: currency code on right", args{mm(0, 0), mmc(0, 0, "YYY")}, mm(0, 0), ErrMismatchingCurrency},
{"Error: currency code mismatch", args{mmc(0, 0, "AAA"), mmc(0, 0, "BBB")}, mm(0, 0), ErrMismatchingCurrency},
{"Error: invalid +/-", args{mm(+1, -1), mm(0, 0)}, mm(0, 0), ErrInvalidValue},
{"Error: invalid -/+", args{mm(0, 0), mm(-1, +2)}, mm(0, 0), ErrInvalidValue},
{"Error: invalid nanos", args{mm(0, 1000000000), mm(1, 0)}, mm(0, 0), ErrInvalidValue},
{"both positive (no carry)", args{mm(2, 200000000), mm(2, 200000000)}, mm(4, 400000000), nil},
{"both positive (nanos=max)", args{mm(2, 111111111), mm(2, 888888888)}, mm(4, 999999999), nil},
{"both positive (carry)", args{mm(2, 200000000), mm(2, 900000000)}, mm(5, 100000000), nil},
{"both negative (no carry)", args{mm(-2, -200000000), mm(-2, -200000000)}, mm(-4, -400000000), nil},
{"both negative (carry)", args{mm(-2, -200000000), mm(-2, -900000000)}, mm(-5, -100000000), nil},
{"mixed (larger positive, just decimals)", args{mm(11, 0), mm(-2, 0)}, mm(9, 0), nil},
{"mixed (larger negative, just decimals)", args{mm(-11, 0), mm(2, 0)}, mm(-9, 0), nil},
{"mixed (larger positive, no borrow)", args{mm(11, 100000000), mm(-2, -100000000)}, mm(9, 0), nil},
{"mixed (larger positive, with borrow)", args{mm(11, 100000000), mm(-2, -9000000 /*.09*/)}, mm(9, 91000000 /*.091*/), nil},
{"mixed (larger negative, no borrow)", args{mm(-11, -100000000), mm(2, 100000000)}, mm(-9, 0), nil},
{"mixed (larger negative, with borrow)", args{mm(-11, -100000000), mm(2, 9000000 /*.09*/)}, mm(-9, -91000000 /*.091*/), nil},
{"0+negative", args{mm(0, 0), mm(-2, -100000000)}, mm(-2, -100000000), nil},
{"negative+0", args{mm(-2, -100000000), mm(0, 0)}, mm(-2, -100000000), nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Sum(tt.args.l, tt.args.r)
if err != tt.wantErr {
t.Errorf("Sum([%v],[%v]): expected err=\"%v\" got=\"%v\"", tt.args.l, tt.args.r, tt.wantErr, err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Sum([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,79 @@
// 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.
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
)
/*
As part of an optional Google Cloud demo, you can run an additional "packaging" microservice (HTTP server).
This file contains code related to the frontend and the "packaging" microservice.
*/
var (
packagingServiceUrl string
)
type PackagingInfo struct {
Weight float32 `json:"weight"`
Width float32 `json:"width"`
Height float32 `json:"height"`
Depth float32 `json:"depth"`
}
// init() is a special function in Golang that will run when this package is imported.
func init() {
packagingServiceUrl = os.Getenv("PACKAGING_SERVICE_URL")
}
func isPackagingServiceConfigured() bool {
return packagingServiceUrl != ""
}
func httpGetPackagingInfo(productId string) (*PackagingInfo, error) {
// Make the GET request
url := packagingServiceUrl + "/" + productId
fmt.Println("Requesting packaging info from URL: ", url)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Check the response status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unexpected status code: %d", resp.StatusCode)
}
// Read the JSON response body
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Decode the JSON response into a PackagingInfo struct
var packagingInfo PackagingInfo
err = json.Unmarshal(responseBody, &packagingInfo)
if err != nil {
return nil, err
}
return &packagingInfo, nil
}

127
src/frontend/rpc.go Normal file
View File

@@ -0,0 +1,127 @@
// Copyright 2018 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 main
import (
"context"
"time"
pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
"github.com/pkg/errors"
)
const (
avoidNoopCurrencyConversionRPC = false
)
func (fe *frontendServer) getCurrencies(ctx context.Context) ([]string, error) {
currs, err := pb.NewCurrencyServiceClient(fe.currencySvcConn).
GetSupportedCurrencies(ctx, &pb.Empty{})
if err != nil {
return nil, err
}
var out []string
for _, c := range currs.CurrencyCodes {
if _, ok := whitelistedCurrencies[c]; ok {
out = append(out, c)
}
}
return out, nil
}
func (fe *frontendServer) getProducts(ctx context.Context) ([]*pb.Product, error) {
resp, err := pb.NewProductCatalogServiceClient(fe.productCatalogSvcConn).
ListProducts(ctx, &pb.Empty{})
return resp.GetProducts(), err
}
func (fe *frontendServer) getProduct(ctx context.Context, id string) (*pb.Product, error) {
resp, err := pb.NewProductCatalogServiceClient(fe.productCatalogSvcConn).
GetProduct(ctx, &pb.GetProductRequest{Id: id})
return resp, err
}
func (fe *frontendServer) getCart(ctx context.Context, userID string) ([]*pb.CartItem, error) {
resp, err := pb.NewCartServiceClient(fe.cartSvcConn).GetCart(ctx, &pb.GetCartRequest{UserId: userID})
return resp.GetItems(), err
}
func (fe *frontendServer) emptyCart(ctx context.Context, userID string) error {
_, err := pb.NewCartServiceClient(fe.cartSvcConn).EmptyCart(ctx, &pb.EmptyCartRequest{UserId: userID})
return err
}
func (fe *frontendServer) insertCart(ctx context.Context, userID, productID string, quantity int32) error {
_, err := pb.NewCartServiceClient(fe.cartSvcConn).AddItem(ctx, &pb.AddItemRequest{
UserId: userID,
Item: &pb.CartItem{
ProductId: productID,
Quantity: quantity},
})
return err
}
func (fe *frontendServer) convertCurrency(ctx context.Context, money *pb.Money, currency string) (*pb.Money, error) {
if avoidNoopCurrencyConversionRPC && money.GetCurrencyCode() == currency {
return money, nil
}
return pb.NewCurrencyServiceClient(fe.currencySvcConn).
Convert(ctx, &pb.CurrencyConversionRequest{
From: money,
ToCode: currency})
}
func (fe *frontendServer) getShippingQuote(ctx context.Context, items []*pb.CartItem, currency string) (*pb.Money, error) {
quote, err := pb.NewShippingServiceClient(fe.shippingSvcConn).GetQuote(ctx,
&pb.GetQuoteRequest{
Address: nil,
Items: items})
if err != nil {
return nil, err
}
localized, err := fe.convertCurrency(ctx, quote.GetCostUsd(), currency)
return localized, errors.Wrap(err, "failed to convert currency for shipping cost")
}
func (fe *frontendServer) getRecommendations(ctx context.Context, userID string, productIDs []string) ([]*pb.Product, error) {
resp, err := pb.NewRecommendationServiceClient(fe.recommendationSvcConn).ListRecommendations(ctx,
&pb.ListRecommendationsRequest{UserId: userID, ProductIds: productIDs})
if err != nil {
return nil, err
}
out := make([]*pb.Product, len(resp.GetProductIds()))
for i, v := range resp.GetProductIds() {
p, err := fe.getProduct(ctx, v)
if err != nil {
return nil, errors.Wrapf(err, "failed to get recommended product info (#%s)", v)
}
out[i] = p
}
if len(out) > 4 {
out = out[:4] // take only first four to fit the UI
}
return out, err
}
func (fe *frontendServer) getAd(ctx context.Context, ctxKeys []string) ([]*pb.Ad, error) {
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*100)
defer cancel()
resp, err := pb.NewAdServiceClient(fe.adSvcConn).GetAds(ctx, &pb.AdRequest{
ContextKeys: ctxKeys,
})
return resp.GetAds(), errors.Wrap(err, "failed to get ads")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4f, 2020-05-01)"
sodipodi:docname="Cymbal_NavLogo.svg"
id="svg835"
version="1.1"
fill="none"
viewBox="0 0 85.633156 28.251238"
height="28.251238"
width="85.633156">
<metadata
id="metadata841">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs839" />
<sodipodi:namedview
inkscape:current-layer="svg835"
inkscape:window-maximized="0"
inkscape:window-y="25"
inkscape:window-x="0"
inkscape:cy="17.327124"
inkscape:cx="59.485955"
inkscape:zoom="13.692308"
fit-margin-bottom="0"
fit-margin-right="0"
fit-margin-left="0"
fit-margin-top="0"
inkscape:snap-global="false"
showguides="false"
showgrid="false"
id="namedview837"
inkscape:window-height="1096"
inkscape:window-width="2277"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<path
id="path833"
fill="#000000"
d="m 38.248749,11.252164 c -3.156207,0 -5.719697,-2.45769 -5.719697,-5.64087 C 32.529052,2.428104 35.062912,0 38.219119,0 c 1.95596,0 3.689611,0.903134 4.771411,2.546554 0.3556,0.53299 0.6371,1.14002 0.726,1.77666 h -2.904281 c -0.42972,-1.11042 -1.4077,-1.89511 -2.62277,-1.89511 -1.76333,0 -2.904307,1.49536 -2.904307,3.16838 0,1.67303 1.140977,3.2276 2.919127,3.2276 1.21507,0 2.11896,-0.75508 2.60795,-1.83588 h 2.904281 c -0.6223,2.5761596 -2.845011,4.26396 -5.467781,4.26396 z" />
<g
transform="translate(63.326481,0.033206)"
id="g867"
style="fill:none">
<path
d="m 5.56306,11.2706 c -0.8298,0 -1.9115,-0.3553 -2.3264,-1.14 H 3.20702 v 0.8587 H 0.880615 V 0.166504 H 3.31075 V 3.51255 C 3.94792,2.8463 4.7629,2.59461 5.66679,2.59461 c 0.59271,0 1.17061,0.14805 1.71887,0.39975 1.5707,0.72547 2.28196,2.20602 2.28196,3.89385 0,2.44291 -1.5707,4.38239 -4.10456,4.38239 z M 5.25189,4.85985 c -1.15579,0 -2.01523,0.90314 -2.01523,2.04316 0,1.15483 0.78534,2.08758 1.97077,2.08758 1.17062,0 2.03006,-0.91794 2.03006,-2.07277 0,-1.11042 -0.84462,-2.05797 -1.9856,-2.05797 z"
fill="#000000"
id="path856" />
</g>
<g
transform="translate(42.505839,2.180044)"
id="g854"
style="fill:none">
<path
d="M 20.8287,4.56236 V 9.00402 H 18.3986 V 4.68081 c 0,-0.88833 -0.1186,-1.80628 -1.2447,-1.80628 -1.0817,0 -1.3484,0.75509 -1.3484,1.68783 V 9.00402 H 13.3753 V 4.57717 c 0,-0.82911 -0.1926,-1.70264 -1.2151,-1.70264 -1.1261,0 -1.378,0.84392 -1.378,1.80628 V 9.00402 H 8.35205 V 3.8517 L 2.9287,11.7134 H 0.0688477 L 1.684,9.38897 V 0.875788 H 4.11413 V 5.80603 H 4.18822 L 7.53706,0.875788 H 10.6192 V 1.73451 h 0.0296 c 0.4742,-0.829111 1.3188,-1.140028 2.2375,-1.140028 1.0521,0 1.8523,0.532998 2.3412,1.450938 0.5928,-0.91794 1.4818,-1.450938 2.5784,-1.450938 0.8001,0 1.5707,0.236889 2.1337,0.814308 0.9336,0.94755 0.8891,1.90991 0.8891,3.15357 z"
fill="#000000"
id="path843" />
</g>
<g
transform="translate(73.255785,2.027234)"
id="g880"
style="fill:none">
<path
d="M 6.4988,8.98937 V 8.11585 H 6.46917 C 6.02463,8.94496 4.97256,9.27068 4.0983,9.27068 c -2.50423,0 -4.08975508,-1.93953 -4.08975508,-4.35283 0,-2.36888 1.64479508,-4.338016 4.08975508,-4.338016 0.88907,0 1.8967,0.340527 2.37087,1.140026 H 6.4988 V 0.861139 H 8.92895 V 8.98937 Z M 4.45393,2.85988 c -1.18544,0 -2.00042,0.93275 -2.00042,2.08758 0,1.12522 0.87426,2.04317 2.01524,2.04317 1.18544,0 2.03005,-0.90314 2.03005,-2.07278 0,-1.16963 -0.85944,-2.05797 -2.04487,-2.05797 z"
fill="#000000"
id="path869" />
</g>
<g
transform="translate(83.133197,-0.02268)"
id="g893"
style="fill:none">
<path
d="M 0.0698242,10.9893 V 0.166504 H 2.49996 V 11.0041 H 0.0698242 Z"
fill="#000000"
id="path882" />
</g>
<g
transform="translate(32.708522,14.032818)"
id="g906"
style="fill:none">
<path
d="m 4.18626,11.7607 c -2.19305,0 -3.926739,-1.1253 -3.926739,-3.44973 V 7.95564 H 3.03047 c 0,0.71067 0.32599,1.48056 1.12616,1.48056 0.57789,0 1.05206,-0.45898 1.05206,-1.03639 C 5.20869,7.68914 4.61598,7.46706 4.0529,7.21537 3.7269,7.06731 3.40091,6.93406 3.08974,6.80081 1.68204,6.20859 0.437336,5.39428 0.437336,3.69165 c 0,-2.02836 1.955964,-3.197998 3.808194,-3.197998 1.05207,0 2.25232,0.39975 2.93394,1.228858 0.56308,0.69586 0.69644,1.28808 0.72608,2.1468 H 5.16424 C 5.07533,3.26229 4.86788,2.81812 4.17144,2.81812 c -0.48899,0 -0.94834,0.34053 -0.94834,0.85872 0,0.16286 0.01482,0.32572 0.10372,0.45897 0.26672,0.44417 1.68924,0.99198 2.1486,1.19925 C 6.91275,6.00131 8.02409,6.74159 8.02409,8.44422 7.97964,10.7095 6.33485,11.7607 4.18626,11.7607 Z"
fill="#000000"
id="path895" />
</g>
<g
transform="translate(40.443303,14.020917)"
id="g919"
style="fill:none">
<path
d="M 5.89196,11.5535 V 7.12668 c 0,-0.88833 -0.25191,-1.70264 -1.30398,-1.70264 -1.05207,0 -1.36324,0.69586 -1.36324,1.61381 V 11.5535 H 0.779785 V 0.686279 H 3.22474 V 4.15077 H 3.25437 C 3.68409,3.38088 4.52871,3.12919 5.35852,3.12919 c 0.80016,0 1.68923,0.2813 2.22268,0.88833 0.78535,0.88833 0.77053,1.8655 0.77053,2.97591 V 11.5387 H 5.89196 Z"
fill="#000000"
id="path908" />
</g>
<g
transform="translate(48.369792,16.988328)"
id="g932"
style="fill:none">
<path
d="m 5.38968,8.80519 c -2.50422,0 -4.40091,-1.83589 -4.40091,-4.33802 0,-2.50214 1.89669,-4.33802 4.40091,-4.33802 2.50423,0 4.40092,1.83588 4.40092,4.33802 0,2.50213 -1.89669,4.33802 -4.40092,4.33802 z m 0,-6.38118 c -1.12616,0 -1.95596,0.94755 -1.95596,2.05796 0,1.11042 0.8298,2.05797 1.95596,2.05797 1.12616,0 1.95597,-0.94755 1.95597,-2.05797 0,-1.11041 -0.81499,-2.05796 -1.95597,-2.05796 z"
fill="#000000"
id="path921" />
</g>
<g
transform="translate(58.368261,16.973537)"
id="g945"
style="fill:none">
<path
d="m 5.40643,8.83479 c -0.96316,0 -1.79296,-0.29611 -2.45977,-1.00677 V 11.2777 H 0.501709 V 0.410455 H 2.8133 V 1.29879 H 2.82812 2.85775 C 3.49492,0.499289 4.39882,0.12915 5.40643,0.12915 7.9403,0.12915 9.39245,2.20192 9.39245,4.556 9.37763,6.83605 7.79212,8.83479 5.40643,8.83479 Z M 4.90263,2.4092 c -1.17062,0 -2.04488,0.91794 -2.04488,2.08758 0,1.14002 0.87426,2.07277 2.04488,2.07277 1.15579,0 2.04487,-0.91794 2.04487,-2.07277 C 6.93268,3.32714 6.07324,2.4092 4.90263,2.4092 Z"
fill="#000000"
id="path934" />
</g>
<g
transform="translate(67.2903,17.010531)"
id="g958"
style="fill:none">
<path
d="m 3.94884,8.83463 c -1.70405,0 -3.126569,-0.90314 -3.319201,-2.6946 H 3.05977 c 0.07409,0.50338 0.44454,0.75508 0.91871,0.75508 0.37045,0 0.85943,-0.20728 0.85943,-0.63664 0,-0.59222 -0.6668,-0.76988 -1.09652,-0.93274 C 3.38576,5.20728 3.03013,5.08884 2.68932,4.95559 1.72616,4.60026 0.74818,3.97843 0.74818,2.8236 c 0,-1.70264 1.58551,-2.679801 3.15621,-2.679801 1.62997,0 2.96357,0.843913 3.12657,2.576161 H 4.63046 C 4.61565,2.26098 4.28965,2.09812 3.85993,2.09812 c -0.32599,0 -0.69643,0.17767 -0.69643,0.54781 0,1.27327 4.10454,0.56261 4.10454,3.43488 0,1.82107 -1.6596,2.75382 -3.3192,2.75382 z"
fill="#000000"
id="path947" />
</g>
<g
transform="translate(12.600322,11.81969)"
id="g1017"
style="fill:none">
<path
d="M 13.0939,13.6186 V 4.09905 c 0,-1.7213 -1.408,-3.143239 -3.14566,-3.143239 H 3.29732 c -1.72265,0 -3.145709,1.406979 -3.145709,3.143239 v 6.39125 c 0,1.7213 1.408079,3.1433 3.145709,3.1433 z"
fill="#840237"
id="path1006" />
</g>
<g
transform="translate(5.816672,0.035276)"
id="g1030"
style="fill:none">
<path
d="M 13.6377,9.82748 V 3.48112 c 0,-1.75124 -1.4231,-3.188151 -3.1907,-3.188151 H 3.84102 c -1.72265,0 -3.145708,1.406971 -3.145708,3.143241 V 12.9558 H 10.477 c 1.7376,0 3.1607,-1.407 3.1607,-3.12832 z"
fill="#ce0631"
id="path1019" />
</g>
<g
transform="translate(-0.239258,12.021229)"
id="g1043"
style="fill:none">
<path
d="M 13.1666,10.4903 V 4.09905 c 0,-1.7213 -1.4081,-3.143239 -3.1457,-3.143239 H 0.239258 V 10.4753 c 0,1.7213 1.408082,3.1433 3.145712,3.1433 h 6.65093 c 1.7226,0 3.1307,-1.407 3.1307,-3.1283 z"
fill="#ff6631"
id="path1032" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4f, 2020-05-01)"
height="11.002236"
width="10.434605"
sodipodi:docname="Hipster_CartIcon.svg"
version="1.1"
viewBox="0 0 10.434605 11.002236"
data-name="Layer 1"
id="Layer_1">
<metadata
id="metadata1201">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Hipster</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:current-layer="g1196"
inkscape:window-maximized="0"
inkscape:window-y="25"
inkscape:window-x="625"
inkscape:cy="-0.89051508"
inkscape:cx="22.434662"
inkscape:zoom="20.841972"
fit-margin-bottom="0"
fit-margin-right="0"
fit-margin-left="0"
fit-margin-top="0"
showgrid="false"
id="namedview1199"
inkscape:window-height="1387"
inkscape:window-width="1935"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<defs
id="defs1188">
<style
id="style1186">.cls-1{fill:#b4b2bb}</style>
</defs>
<title
id="title1190">Hipster</title>
<g
transform="translate(-4.4609375,-5.5)"
id="g1196">
<path
sodipodi:nodetypes="ccccccsccccccccccccccccccssssssss"
d="m 4.4609375,5.5 v 1.0996094 h 1.0996094 l 2,4.1699216 -0.75,1.34961 c -0.083803,0.164211 -0.1291659,0.346898 -0.1308594,0.53125 0,0.607513 0.4920962,1.099609 1.0996094,1.099609 H 14.380859 V 12.650391 H 8 c -0.081192,0.0058 -0.1505985,-0.05923 -0.1503906,-0.140625 v -0.06055 l 0.4902344,-0.898438 h 4.0996092 c 0.414076,0.01348 0.800604,-0.207156 1,-0.570312 l 1.456089,-4.4008431 -8.1260108,0.019984 v 0 L 6.25,5.5 Z M 7.1729534,7.576157 13.673828,7.54223 12.681641,10.603516 8.5214844,10.589846 Z m 0.5868122,6.724624 c -0.9800635,0 -1.4702693,1.184027 -0.7773437,1.876953 0.6929255,0.692925 1.8769531,0.20272 1.8769531,-0.777343 0,-0.607513 -0.4920962,-1.09961 -1.0996094,-1.09961 z m 5.5000004,0 c -0.980063,0 -1.470269,1.184027 -0.777344,1.876953 0.692926,0.692925 1.876953,0.20272 1.876953,-0.777343 0,-0.607513 -0.492096,-1.09961 -1.099609,-1.09961 z"
id="path1192" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#b4b2bb}</style></defs><title>Hipster</title><g><path d="M14.5,5.5h-9A1.12,1.12,0,0,0,4.38,6.62v6.76A1.12,1.12,0,0,0,5.5,14.5h9a1.12,1.12,0,0,0,1.12-1.12V6.62A1.12,1.12,0,0,0,14.5,5.5Zm0,7.88h-9V10h9Zm0-5.63h-9V6.62h9Z" class="cls-1"/><path d="M10,2a8,8,0,1,1-8,8,8,8,0,0,1,8-8m0-2A10,10,0,1,0,20,10,10,10,0,0,0,10,0Z" class="cls-1"/></g></svg>

After

Width:  |  Height:  |  Size: 462 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#605f64}</style></defs><title>Hipster</title><g><path d="M10.28,9.21c-1.64-.43-2.16-.87-2.16-1.56s.72-1.33,2-1.33,1.76.61,1.8,1.51h1.6a2.88,2.88,0,0,0-2.32-2.75V3.5H9V5.06A2.82,2.82,0,0,0,6.45,7.67c0,1.67,1.38,2.5,3.4,3,1.8.43,2.16,1.07,2.16,1.74,0,.5-.35,1.29-1.95,1.29S8,13,7.91,12.17H6.32A3,3,0,0,0,9,14.93V16.5h2.17V15c1.41-.27,2.53-1.09,2.53-2.57C13.68,10.33,11.92,9.63,10.28,9.21Z" class="cls-1"/><path d="M10,2a8,8,0,1,1-8,8,8,8,0,0,1,8-8m0-2A10,10,0,1,0,20,10,10,10,0,0,0,10,0Z" class="cls-1"/></g></svg>

After

Width:  |  Height:  |  Size: 630 B

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4f, 2020-05-01)"
sodipodi:docname="Hipster_DownArrow.svg"
version="1.1"
viewBox="0 0 10 6"
data-name="Layer 1"
id="Layer_1">
<metadata
id="metadata1241">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:current-layer="g1805"
inkscape:window-maximized="0"
inkscape:window-y="432"
inkscape:window-x="453"
inkscape:cy="7.8920446"
inkscape:cx="9.2114456"
inkscape:zoom="27.4"
showgrid="false"
id="namedview1239"
inkscape:window-height="815"
inkscape:window-width="1338"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<defs
id="defs1232">
<style
id="style1230">.cls-1{fill:#605f64}</style>
</defs>
<title
id="title1234">Hipster</title>
<g
style="opacity:1"
id="g1805">
<path
sodipodi:nodetypes="cccc"
id="path1841"
d="M 0.04897564,0.08494176 5.0087697,6.0165613 9.9685637,0.08494176 Z"
style="fill:#5c6063;stroke:none;stroke-width:0.08082460000000000px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#111}</style></defs><title>Hipster</title><g><path d="M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0ZM14.42,17.76H10.24v4.87h3.28v2.31H10.24v6.68H7.69V15.45h6.73ZM21,31.62l-.44-2.94H17.44L17,31.62H14.67l2.59-16.17H21l2.58,16.17ZM32.11,20.9h-2.4V19.16c0-1.15-.51-1.59-1.32-1.59s-1.32.44-1.32,1.59V27.9c0,1.15.51,1.57,1.32,1.57s1.32-.42,1.32-1.57V25.59h2.4v2.15c0,2.58-1.29,4.06-3.79,4.06s-3.79-1.48-3.79-4.06V19.33c0-2.59,1.29-4.07,3.79-4.07s3.79,1.48,3.79,4.07Zm8.47-3.14H36.19v4.5h3.49v2.31H36.19v4.74h4.39v2.31H33.65V15.45h6.93Z" class="cls-1"/><polygon points="17.77 26.49 20.21 26.49 18.99 18.31 17.77 26.49" class="cls-1"/></g></svg>

After

Width:  |  Height:  |  Size: 750 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#605f64}</style></defs><title>Hipster</title><g><path d="M9,16h2V14H9ZM10,4A4,4,0,0,0,6,8H8a2,2,0,0,1,4,0c0,2-3,1.75-3,5h2c0-2.25,3-2.5,3-5A4,4,0,0,0,10,4Z" class="cls-1"/><path d="M10,2a8,8,0,1,1-8,8,8,8,0,0,1,8-8m0-2A10,10,0,1,0,20,10,10,10,0,0,0,10,0Z" class="cls-1"/></g></svg>

After

Width:  |  Height:  |  Size: 399 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,235 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Layer_1"
data-name="Layer 1"
viewBox="0 0 625.15 469.57"
version="1.1"
sodipodi:docname="Hipster_HeroLogoMaroon.svg"
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview213"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.0051749"
inkscape:cx="299.94779"
inkscape:cy="275.0765"
inkscape:window-width="1724"
inkscape:window-height="905"
inkscape:window-x="204"
inkscape:window-y="1141"
inkscape:window-maximized="0"
inkscape:current-layer="g210" />
<defs
id="defs134">
<style
id="style132">.cls-1{fill:#4bc7c7}</style>
</defs>
<title
id="title136">Hipster</title>
<g
id="g210"
style="stroke-width:1.75748031;stroke-dasharray:none;paint-order:stroke markers fill;stroke:#ffffff;stroke-opacity:1">
<g
id="g1317-3"
transform="matrix(1.0146883,0,0,1.0241514,-4.2853293,-6.1372338)"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.72401905;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
<g
id="g196-5"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
transform="matrix(0.97135514,0,0,0.96186606,8.9536653,8.9521229)">
<path
d="m 214.76,32.83 -4.44,-9 a 231,231 0 0 1 202.19,-1.14 l -4.34,9 a 221,221 0 0 0 -193.41,1.09 z"
class="cls-1"
id="path138-2"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<g
id="g144-2"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
<path
d="m 120.84,339.47 q -1.55,-2.72 -3,-5.47 h -11.27 q 2.66,5.28 5.58,10.42 a 232,232 0 0 0 79.29,82.22 l 5.26,-8.5 a 222,222 0 0 1 -75.86,-78.67 z"
class="cls-1"
id="path140-6"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 117.78,127 a 221.75,221.75 0 0 1 21.57,-32.94 l -7.85,-6.2 a 231.18,231.18 0 0 0 -25,39.14 z"
class="cls-1"
id="path142-6"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
</g>
<g
id="g158-7"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
<path
d="m 198.23,116.53 -0.38,-0.86 -0.29,-0.77 -0.58,-1.73 -0.19,-0.86 -0.1,-0.87 -0.19,-0.86 -0.29,-2.6 v -1.82 l 0.1,-0.87 v -2.69 l 0.19,-1.72 0.19,-1 0.1,-0.87 0.19,-0.77 0.19,-0.86 0.29,-1 0.39,-1.06 0.57,-1.92 0.39,-1.06 0.38,-0.86 0.39,-0.77 0.38,-0.86 0.38,-0.77 0.39,-0.87 0.48,-0.76 0.48,-0.68 1,-0.57 1.06,-0.29 0.86,0.09 0.77,0.29 0.77,0.48 0.76,0.77 0.49,1.06 v 0.86 l -0.49,1.15 -0.47,1.25 -0.29,0.58 -0.39,1 -0.38,0.77 -0.29,0.86 -0.19,0.87 -0.29,1.15 -0.29,1.25 -0.19,0.86 -0.1,0.87 -0.19,0.86 -0.09,0.87 v 0.76 l -0.2,1.73 v 0.87 l 0.1,1 v 1.73 l 0.1,0.86 0.19,0.87 0.19,1.24 0.29,1.54 0.38,1.25 0.29,0.86 0.77,1.54 0.58,0.77 0.47,0.58 0.58,0.67 1.35,1.15 0.67,0.48 0.77,0.38 0.86,0.29 1.25,0.29 1.15,0.19 0.87,0.2 h 2.49 l 0.87,-0.2 0.86,-0.09 0.87,-0.29 1,-0.39 0.77,-0.38 1.44,-0.86 0.77,-0.48 1.35,-1 1.92,-1.93 0.57,-0.67 0.48,-0.67 0.48,-0.77 0.58,-0.86 0.38,-0.48 1.06,-1.54 0.38,-0.67 0.48,-0.77 0.39,-0.86 0.48,-0.87 0.67,-1.44 0.3,-0.83 0.38,-0.77 0.1,-0.77 -0.29,-0.39 -0.57,-0.19 -0.87,-0.09 -1,-0.1 -0.86,-0.19 -0.77,-0.19 -0.87,-0.29 -0.86,-0.19 -0.77,-0.29 -0.77,-0.39 -2.3,-0.86 -1.06,-0.38 -2.88,-1.73 -0.77,-0.48 -1.44,-1 -0.67,-0.58 -3.65,-3.65 -0.48,-0.67 -0.58,-0.67 -1,-1.45 -0.38,-0.67 -0.39,-0.86 -0.77,-1.54 -0.38,-1.06 -0.38,-1 -0.29,-1 -0.39,-1.05 -0.28,-0.87 -0.2,-1.73 -0.09,-1 v -0.4 l -0.19,-1.92 0.09,-0.87 0.1,-1 0.09,-0.57 0.2,-1.06 0.24,-1.08 0.38,-1.25 0.29,-0.87 0.48,-1.15 0.48,-1.06 0.38,-0.86 0.48,-0.67 1.16,-1.35 0.57,-0.57 1.38,-1.22 1.05,-0.76 1.35,-0.74 2.69,-1.35 0.86,-0.29 1.25,-0.28 1.15,-0.29 1,-0.19 1.16,-0.1 h 2.21 l 1.53,0.19 1.92,0.1 1.16,0.19 1.25,0.29 0.86,0.38 1.73,0.58 1.05,0.57 1.64,1 1,0.57 0.67,0.48 0.67,0.58 1.15,1.34 0.49,0.68 0.48,0.76 0.48,0.68 0.38,0.76 0.38,0.87 0.77,1.54 0.29,0.76 0.29,0.87 0.38,1.73 0.19,1.25 0.29,1.15 0.1,1 0.1,0.58 0.09,1 0.1,0.87 v 0.86 l 0.19,1.73 v 0.86 l -0.1,1 -0.09,0.87 -0.19,0.77 v 1 l -0.1,0.58 v 1.06 l -0.19,0.86 -0.28,1.14 -0.19,1.25 -0.19,1 -0.1,0.87 -0.09,0.76 -0.19,0.87 -0.29,0.86 v 1.06 l 0.57,0.58 h 1.06 l 0.87,-0.2 1.05,-0.28 0.58,-0.2 1.82,-0.48 1.06,-0.38 1.25,-0.48 1,-0.38 0.87,-0.49 0.58,-0.28 0.86,-0.48 0.86,-0.39 0.77,-0.09 0.48,0.38 0.1,0.86 -0.19,0.77 -0.29,0.87 -0.39,0.77 -0.57,0.57 -0.77,0.48 -1,0.39 -0.48,0.28 -1.73,0.87 -0.86,0.38 -0.77,0.29 -1,0.39 -0.58,0.19 -1,0.48 -0.77,0.29 -0.77,0.19 -0.86,0.19 -1,0.29 -1,0.19 -0.87,0.1 -0.76,0.19 -0.58,0.48 -0.49,1.49 -0.29,0.86 -0.51,1.14 -1.44,2.88 -0.29,0.48 -0.48,0.87 -0.38,0.86 -1,1.54 -0.38,0.48 -0.58,0.86 -0.48,0.68 -1.11,1.35 -0.48,0.77 -0.58,0.67 -0.67,0.58 -0.68,0.67 -0.57,0.48 -1.25,1.25 -1.34,1.15 -0.77,0.48 -1.06,0.58 -1.15,0.67 -0.87,0.48 -0.76,0.39 -0.87,0.28 -0.77,0.2 -0.86,0.28 -0.87,0.2 -0.86,0.28 -1,0.1 -0.58,0.1 -1,0.09 -0.86,0.1 h -2.61 l -1.15,-0.19 -1.06,-0.1 -2.39,-0.51 -1.15,-0.29 -0.87,-0.29 -0.77,-0.28 -0.86,-0.49 -1.92,-1.34 -0.77,-0.67 -0.58,-0.58 -0.67,-0.57 -0.58,-0.58 -0.57,-0.77 -0.67,-1 -0.58,-1.06 -0.48,-0.86 z m 17.1,-39.86 v 2.6 l 0.09,1 0.2,0.86 0.09,0.86 0.58,1.73 0.29,0.77 0.38,0.87 0.38,0.76 0.48,0.68 0.48,0.86 0.39,0.77 1.15,1.34 0.67,0.68 0.39,0.48 1.34,1.34 0.67,0.58 0.77,0.57 0.68,0.48 0.76,0.48 0.68,0.48 1,0.49 0.57,0.19 1.06,0.48 0.58,0.19 1,0.38 2.4,0.77 1,0.19 0.86,0.2 0.77,-0.1 0.67,-0.67 0.39,-1.06 0.57,-2.59 0.19,-1.25 0.1,-1.15 0.1,-1 0.29,-2.6 v -0.86 l 0.19,-1.15 v -5.19 l -0.1,-1.15 -0.09,-1 -0.2,-1.16 -0.19,-1.24 -0.38,-1.73 -0.29,-0.87 -0.48,-1.15 -0.39,-1.06 -0.38,-0.86 -0.48,-0.77 -0.48,-0.67 -0.48,-0.48 -0.58,-0.67 -0.76,-0.58 -0.68,-0.48 -0.77,-0.38 -0.86,-0.39 -1.25,-0.29 -1.7,-0.15 -1.2,-0.05 -1,0.1 -1.25,0.19 -1.44,0.48 -1.44,0.77 -1.35,0.87 -1.15,1 -0.87,1 -0.38,0.77 -0.48,1.15 -0.44,1.4 -0.29,1.27 -0.19,1 z"
class="cls-1"
id="path146-8"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 275.16,91.85 -0.76,0.38 -0.68,0.39 -0.67,0.48 -0.77,0.57 -0.67,0.58 -1.54,1.06 -0.67,0.48 -0.67,0.57 -0.67,0.67 -1.16,1.35 -0.67,0.67 -1.15,1.35 -1,1.53 -0.58,0.77 -1.11,2.3 -1.06,3.17 -0.28,1.24 -0.39,1.44 -0.38,1.25 -0.58,2.6 -0.38,1.24 -0.39,1.45 -0.28,1.24 -0.29,0.87 -0.29,1.25 -0.19,1 -0.29,1 -0.1,0.87 -0.19,0.77 -0.29,0.77 -0.57,0.48 -0.87,0.28 -0.86,0.1 -1.25,0.1 -1.46,-0.12 -1.25,-0.19 -0.77,-0.39 -0.58,-0.86 -0.09,-1.06 0.19,-1 0.29,-1.25 0.29,-1.15 0.19,-0.86 0.29,-0.87 0.19,-0.86 0.29,-0.77 0.29,-0.87 0.19,-0.86 0.19,-0.77 0.19,-0.86 0.29,-0.87 0.29,-0.77 0.38,-1.72 0.19,-0.77 0.1,-0.87 0.29,-0.86 0.19,-0.87 0.29,-0.86 0.29,-1.15 0.38,-1.25 0.48,-1.73 0.19,-0.77 0.2,-0.86 0.28,-0.87 0.2,-0.86 0.28,-0.77 0.2,-0.86 0.28,-0.87 0.29,-1.25 0.29,-1.15 0.19,-0.86 0.58,-1.73 0.19,-0.77 0.29,-0.87 0.38,-1.72 0.68,-1 1.24,-0.67 1.54,-0.19 1.54,0.19 1.44,0.48 1,1 0.19,1.16 -0.19,1 -0.19,0.67 -0.29,0.86 -0.19,0.87 -0.29,0.77 -0.19,0.86 -0.29,0.86 -0.19,0.87 -0.1,0.58 0.1,0.09 0.38,-0.48 0.58,-0.67 0.48,-0.67 1.15,-1.35 0.67,-0.57 1.25,-1.06 0.67,-0.58 1.25,-1.25 1.92,-1.53 0.77,-0.48 0.77,-0.39 0.77,-0.48 0.77,-0.29 0.86,-0.28 1.25,-0.29 1.15,-0.19 h 2.21 l 1.54,0.19 1.05,0.19 0.87,0.38 0.86,0.48 0.67,0.68 0.48,0.67 0.48,0.77 0.39,0.77 0.29,0.86 0.29,1.15 0.19,1.16 v 1.82 l -0.1,0.87 v 0.86 l -0.09,0.77 -0.2,0.86 -0.19,1 -0.19,0.58 -0.29,1 -0.29,1.25 -0.38,1.4 -0.39,1.25 -0.28,0.86 -0.29,1.25 -0.39,1.63 -0.38,1.54 -0.38,1.34 -0.2,0.87 -0.19,0.67 -0.19,0.87 -0.29,0.86 -0.19,0.77 -0.58,2.59 -0.09,0.87 v 1.72 l 0.38,0.77 0.67,0.48 0.87,0.1 0.86,-0.29 0.87,-0.38 0.76,-0.48 0.68,-0.48 0.77,-0.58 1.24,-1.25 0.48,-0.67 0.58,-0.77 0.48,-0.67 0.67,-0.77 0.48,-0.67 0.58,-0.87 0.86,-1.44 0.58,-0.86 0.29,-0.48 0.57,-0.87 0.48,-0.86 0.77,-1.54 0.39,-0.86 0.29,-0.77 0.24,-0.88 0.48,-0.77 0.87,-0.58 1.25,0.29 0.76,1 v 1 l -0.28,0.86 -0.39,0.77 -0.38,0.87 -0.39,0.77 -0.48,0.76 -0.38,0.77 -0.58,0.87 -0.28,0.57 -1,1.73 -0.19,0.58 -0.48,0.86 -1,1.54 -0.57,0.86 -0.39,0.48 -0.57,0.77 -0.68,0.87 -0.57,0.67 -1.06,1.15 -0.58,0.67 -1.34,1.16 -0.58,0.57 -0.76,0.58 -0.77,0.48 -1.54,0.77 -0.77,0.29 -0.86,0.28 -0.87,0.2 -0.77,0.09 -1,0.1 h -0.86 l -0.86,-0.1 -0.77,-0.19 -0.87,-0.29 -0.77,-0.29 -0.76,-0.48 -0.58,-0.48 -0.58,-0.67 -0.38,-0.86 -0.29,-0.77 -0.19,-0.87 -0.19,-0.76 V 119 l 0.19,-1.25 0.09,-1 0.29,-1.25 0.87,-3.16 0.29,-1.64 0.52,-1.7 0.38,-1.54 0.39,-1.25 0.29,-0.86 0.38,-1.25 0.38,-1.54 0.29,-1.53 0.39,-1.25 0.38,-1.73 0.1,-0.86 0.38,-1.83 0.1,-0.86 -0.19,-1 -0.58,-1 -0.87,-0.87 -0.86,-0.29 -0.86,-0.09 -0.87,0.09 z"
class="cls-1"
id="path148-4"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 327,79.65 -0.48,0.86 -0.77,1.54 -0.48,1.06 -0.67,1 -0.48,0.68 -0.48,0.76 -0.48,0.68 -0.48,0.86 -0.39,0.58 -0.48,0.77 -0.67,0.86 -0.86,1 -0.87,0.87 -0.38,0.48 -0.67,0.76 -1.25,1.25 -0.58,0.67 -1.03,1.17 -0.67,0.57 -0.77,0.77 -1.05,0.77 -1.44,1.25 -0.77,0.48 -2.11,1.34 -0.69,0.32 -0.77,0.48 -1.05,0.87 -0.67,0.86 -0.39,0.77 -0.19,1.06 -0.29,1.72 v 0.58 l -0.19,1 -0.19,0.87 -0.1,1 v 2 l -0.09,1.25 v 1.92 l 0.19,1.06 0.09,0.58 0.2,1 0.38,0.86 0.38,0.77 0.58,0.67 0.77,0.48 0.77,0.29 1,0.19 H 310 l 0.77,-0.09 0.86,-0.29 1.16,-0.48 1.05,-0.58 0.77,-0.58 1,-0.76 0.87,-0.87 1.15,-1.06 0.77,-0.67 0.6,-0.9 0.39,-0.48 0.57,-0.76 0.39,-0.68 0.57,-0.86 0.39,-0.48 0.57,-0.87 0.77,-1.44 0.48,-0.76 0.58,-0.87 0.48,-0.86 0.48,-1 0.86,-1.25 1,-0.77 1.25,0.19 0.77,1 v 1 l -0.29,0.87 -0.38,0.76 -0.48,0.77 -0.39,0.87 -0.38,0.77 -0.29,0.76 -0.48,0.77 -0.38,0.68 -0.48,0.76 -0.58,0.87 -0.19,0.48 -0.48,0.86 -0.58,0.77 -1,1.35 -0.67,0.86 -0.39,0.38 -0.67,0.87 -0.57,0.67 -0.77,1 -0.87,0.77 -0.67,0.67 -0.57,0.48 -0.68,0.58 -0.86,0.48 -0.77,0.38 -0.86,0.39 -0.77,0.38 -0.77,0.29 -0.87,0.38 -1.15,0.39 -1.92,0.38 -1.15,0.1 -1.06,-0.1 h -0.57 l -1,-0.09 -0.87,-0.2 -0.77,-0.28 -0.86,-0.29 -0.77,-0.39 -0.77,-0.48 -0.67,-0.57 -0.58,-0.58 -0.57,-0.67 -0.39,-0.77 -0.38,-0.86 -0.29,-0.77 -0.19,-0.87 -0.29,-0.86 -0.1,-0.87 -0.19,-1 v -0.57 l -0.19,-1.06 -0.1,-0.86 v -0.87 l 0.1,-0.86 0.1,-1 v -0.58 l 0.09,-1.25 0.29,-2.11 0.1,-1.15 v -0.87 l 0.19,-0.86 0.19,-1 0.67,-3.17 0.29,-1.15 0.29,-1.73 0.19,-0.48 0.29,-1.16 0.19,-0.57 0.39,-1.44 0.28,-1.25 0.2,-0.87 0.28,-1.15 0.39,-1.34 0.09,-0.68 0.39,-1.34 0.38,-1.25 0.29,-0.67 0.38,-1.25 0.39,-1.15 0.38,-0.87 1,-2.49 0.2,-0.58 0.57,-1.35 0.67,-1.53 0.48,-1.15 0.39,-0.87 0.29,-0.57 0.48,-0.87 0.67,-1.63 0.48,-1 0.48,-0.87 0.58,-0.86 0.76,-1.25 1.21,-1.67 0.58,-0.77 1.25,-1.23 0.38,-0.48 0.67,-0.67 0.68,-0.48 1.53,-1.06 1.06,-0.67 1.06,-0.48 0.86,-0.29 0.87,-0.19 h 1.72 l 1,0.19 0.58,0.1 1,0.29 0.77,0.38 1.34,1.16 0.48,0.76 0.68,1 0.48,1.16 0.38,0.86 0.38,1.15 0.2,1.25 v 1.73 l -0.2,2.59 -0.41,1.7 -0.29,0.77 -0.19,1 -0.82,2.23 -0.38,0.77 z m -15.94,2.69 -1.35,4 -0.19,0.87 -0.29,0.86 -0.38,1.25 -0.67,2 -0.29,1 -0.1,0.57 -0.29,1 -0.29,0.87 -0.19,1 0.19,0.58 0.58,-0.39 1,-0.77 3.94,-3.93 0.48,-0.68 0.48,-0.77 0.58,-0.86 0.38,-0.38 0.58,-0.87 0.67,-0.86 0.67,-1 0.67,-1.06 0.48,-0.77 0.39,-0.67 0.48,-0.67 0.77,-1.54 0.38,-1 0.29,-0.58 0.38,-1 0.39,-0.86 0.19,-0.87 0.29,-0.86 0.29,-1.06 0.38,-1 0.29,-1.06 0.38,-1.06 0.1,-1 0.09,-0.86 v -1.79 l -0.19,-0.76 -0.29,-0.77 -0.67,-0.48 -0.77,-0.19 -0.86,0.19 -0.87,0.67 -0.67,0.58 -0.48,0.57 -0.48,0.67 -0.48,0.77 -1,1.44 -0.48,0.87 -0.38,0.86 -0.39,0.77 -0.38,0.87 -0.39,0.76 -0.38,0.87 -0.29,0.77 -0.38,1 -0.58,1.44 -0.38,0.87 z"
class="cls-1"
id="path150-1"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 348.16,105.1 v 1.06 l -0.29,0.86 -0.77,1.54 -0.86,1.44 -0.48,0.86 -0.39,0.77 -0.57,0.87 -0.29,0.57 -0.48,0.87 -0.48,0.77 -0.48,0.67 -0.58,0.77 -0.48,0.67 -1,1.54 -1.73,2 -1.15,1.25 -0.67,0.67 -0.58,0.68 -0.67,0.57 -2.11,1.64 -0.68,0.48 -0.86,0.38 -0.86,0.29 -0.77,0.19 -1.73,0.38 h -1.63 l -1.06,-0.09 -0.77,-0.29 -0.86,-0.29 -0.77,-0.48 -0.67,-0.57 -0.48,-0.68 -0.48,-0.77 -0.39,-0.86 -0.19,-0.77 -0.19,-0.86 -0.1,-0.87 v -0.86 l 0.19,-1.73 0.2,-0.86 0.28,-0.87 0.39,-1.73 0.29,-0.77 0.19,-0.86 0.29,-0.87 0.28,-0.76 0.87,-2.6 0.09,-0.77 0.2,-0.86 0.28,-0.86 0.2,-0.87 0.19,-0.77 0.57,-1.73 0.29,-1.15 0.29,-1.25 0.58,-1.73 0.19,-0.86 0.19,-0.67 0.19,-0.87 0.39,-1 0.29,-0.77 0.57,-2.59 0.29,-0.86 0.29,-1.16 0.38,-1.24 0.29,-0.87 0.39,-0.77 0.57,-0.48 0.87,-0.38 0.76,-0.1 1,0.1 1.73,0.19 1.54,0.58 0.48,0.67 v 0.86 l -0.19,0.87 -0.29,1.15 -0.29,1.25 -0.57,1.73 -0.2,0.77 -0.19,0.86 -0.29,1 -0.28,1.13 -1,2.89 -0.19,0.86 -0.38,1.25 -0.67,2 -0.2,0.86 -0.19,0.77 -0.19,0.86 -0.67,1.64 -0.19,0.86 -0.68,2.4 -0.57,1.73 -0.2,0.86 -0.28,1.25 -0.29,1.54 -0.19,1.63 0.19,1.44 0.77,1.06 1.05,0.29 0.87,-0.1 0.86,-0.38 0.67,-0.48 0.77,-0.58 3.17,-3.17 0.58,-0.67 0.48,-0.67 0.58,-0.77 1,-1.54 0.57,-0.67 0.39,-0.58 0.48,-0.77 0.57,-0.86 0.39,-0.86 0.77,-1.64 1.15,-2.3 0.48,-0.77 0.86,-0.48 1.25,0.19 z m -15.46,-29.48 0.48,-0.77 0.76,-0.67 0.77,-0.49 1,-0.28 0.87,-0.2 h 1 l 0.86,0.2 0.77,0.48 0.67,0.57 0.68,0.68 0.48,0.76 0.28,0.87 0.1,0.86 v 1 l -0.29,0.87 -0.48,0.77 -0.65,0.73 -0.68,0.58 -0.77,0.58 -0.86,0.38 -1,0.19 -0.86,-0.09 -1,-0.2 -1.54,-1 -0.58,-0.67 -0.48,-0.77 -0.19,-0.86 -0.1,-0.87 0.1,-0.86 0.19,-1 z"
class="cls-1"
id="path152-7"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 365.93,91.85 -0.77,0.38 -0.67,0.39 -0.67,0.48 -0.77,0.57 -0.68,0.58 -1.53,1.06 -0.67,0.48 -0.68,0.57 -0.67,0.67 -1.15,1.35 -0.67,0.67 -1.16,1.35 -1,1.53 -0.57,0.77 -1.12,2.3 -1,3.17 -0.29,1.24 -0.38,1.44 -0.39,1.25 -0.57,2.6 -0.39,1.24 -0.38,1.45 -0.29,1.24 -0.29,0.87 -0.29,1.25 -0.19,1 -0.29,1 -0.09,0.87 -0.2,0.77 -0.28,0.77 -0.58,0.48 -0.86,0.28 -0.87,0.1 -1.25,0.1 -1.44,-0.1 -1.25,-0.19 -0.77,-0.39 -0.57,-0.86 -0.1,-1.06 0.19,-1 0.29,-1.25 0.29,-1.15 0.19,-0.86 0.29,-0.87 0.19,-0.86 0.29,-0.77 0.29,-0.87 0.19,-0.86 0.19,-0.77 0.19,-0.86 0.29,-0.87 0.29,-0.77 0.39,-1.72 0.19,-0.77 0.09,-0.87 0.29,-0.86 0.19,-0.87 0.29,-0.86 0.29,-1.15 0.38,-1.25 0.48,-1.73 0.2,-0.77 0.19,-0.86 0.29,-0.87 0.19,-0.86 0.29,-0.77 0.19,-0.86 0.29,-0.87 0.29,-1.25 0.28,-1.15 0.2,-0.86 0.57,-1.73 0.19,-0.77 0.29,-0.87 0.39,-1.72 0.67,-1 1.25,-0.67 1.53,-0.19 1.54,0.19 1.44,0.48 1,1 0.19,1.16 -0.19,1 -0.19,0.67 -0.29,0.86 -0.19,0.87 -0.29,0.77 -0.19,0.86 -0.29,0.86 -0.19,0.87 -0.1,0.58 0.1,0.09 0.38,-0.48 0.58,-0.67 0.48,-0.67 1.15,-1.35 0.67,-0.57 1.19,-1.05 0.68,-0.58 1.24,-1.25 1.93,-1.53 0.76,-0.48 0.77,-0.39 0.77,-0.48 0.77,-0.29 0.86,-0.28 1.25,-0.29 1.15,-0.19 h 2.21 l 1.54,0.19 1.06,0.19 0.86,0.38 0.87,0.48 0.67,0.68 0.48,0.67 0.48,0.77 0.38,0.77 0.29,0.86 0.29,1.15 0.19,1.16 v 1.82 l -0.1,0.87 v 0.86 l -0.09,0.77 -0.19,0.86 -0.2,1 -0.19,0.58 -0.29,1 -0.28,1.25 -0.39,1.44 -0.38,1.25 -0.29,0.86 -0.29,1.25 -0.41,1.57 -0.39,1.54 -0.38,1.34 -0.19,0.87 -0.2,0.67 -0.19,0.87 -0.29,0.86 -0.19,0.77 -0.57,2.59 -0.1,0.87 v 1.72 l 0.38,0.77 0.68,0.48 0.86,0.1 0.87,-0.29 0.86,-0.38 0.77,-0.48 0.67,-0.48 0.77,-0.58 1.24,-1.24 0.48,-0.67 0.57,-0.77 0.48,-0.67 0.68,-0.77 0.48,-0.67 0.57,-0.87 0.87,-1.44 0.57,-0.86 0.29,-0.48 0.58,-0.87 0.48,-0.86 0.77,-1.54 0.38,-0.86 0.29,-0.77 0.29,-0.86 0.48,-0.77 0.86,-0.58 1.25,0.29 0.77,1 v 1 l -0.29,0.86 -0.38,0.77 -0.39,0.87 -0.38,0.77 -0.48,0.76 -0.39,0.77 -0.57,0.87 -0.29,0.57 -1,1.73 -0.19,0.58 -0.48,0.86 -1,1.54 -0.58,0.86 -0.38,0.48 -0.58,0.77 -0.67,0.87 -0.58,0.67 -1.06,1.15 -0.57,0.67 -1.35,1.16 -0.57,0.57 -0.77,0.58 -0.77,0.48 -1.54,0.77 -0.76,0.29 -0.87,0.28 -0.86,0.2 -0.77,0.09 -1,0.1 h -0.87 l -0.86,-0.1 -0.77,-0.19 -0.86,-0.29 -0.77,-0.29 -0.77,-0.48 -0.58,-0.48 -0.57,-0.67 -0.39,-0.86 -0.29,-0.77 -0.19,-0.87 -0.19,-0.76 v -2.12 l 0.19,-1.25 0.1,-1 0.29,-1.25 0.86,-3.16 0.29,-1.64 0.38,-1.63 0.39,-1.54 0.38,-1.25 0.29,-0.86 0.38,-1.25 0.39,-1.54 0.29,-1.53 0.38,-1.25 0.38,-1.73 0.1,-0.86 0.38,-1.83 0.1,-0.86 -0.19,-1 -0.58,-1 -0.86,-0.87 -0.87,-0.29 -0.86,-0.09 -0.87,0.09 z"
class="cls-1"
id="path154-1"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 419.71,102.8 -0.48,0.67 -1.15,1.34 -1.34,1.19 -0.68,0.48 -0.86,0.48 -0.58,0.19 -1,0.57 -0.57,0.29 -0.87,0.39 -1,0.29 -0.86,0.28 -0.87,0.1 -1,0.19 -1.05,0.1 -1.16,0.19 -0.86,0.1 h -0.87 l -1.72,-0.2 -1.73,-0.09 -0.87,-0.1 -2.59,-0.57 h -0.67 l -0.48,0.38 -0.2,0.67 v 0.87 l -0.09,1 v 1.63 l 0.09,1 0.2,0.77 0.57,1.73 1,1.53 0.58,0.68 0.58,0.57 1.53,1 1,0.29 0.87,0.29 0.67,0.1 0.86,0.19 0.87,0.09 h 1 l 0.86,-0.09 2.5,-0.48 0.87,-0.2 0.86,-0.28 0.77,-0.29 1.63,-0.67 1.54,-0.77 1.34,-1.06 0.65,-0.64 0.68,-0.58 1.34,-1.24 1.15,-1.35 0.48,-0.67 0.58,-0.77 1,-1.34 0.48,-0.77 0.39,-0.77 0.48,-0.77 0.38,-0.77 0.48,-0.67 0.38,-0.77 0.39,-0.86 0.38,-0.77 0.48,-0.77 0.68,-0.48 h 0.86 l 0.86,0.58 0.48,0.86 -0.09,0.87 -0.39,0.86 -0.48,1.06 -0.57,1 -0.48,1.06 -0.58,1.06 -0.58,1 -1,1.53 -0.28,0.58 -0.58,0.86 -1,1.35 -0.58,0.67 -0.57,0.58 -0.58,0.67 -0.67,0.77 -0.29,0.38 -0.67,0.77 -0.58,0.67 -1.28,1.17 -1,0.76 -0.87,0.77 -1.53,1 -2.41,1.06 -0.76,0.29 -0.87,0.29 -1.25,0.19 -1.15,0.29 -2,0.19 -1.25,0.09 -1.05,0.1 h -0.87 l -3.45,-0.38 -0.77,-0.2 -0.87,-0.28 -0.86,-0.39 -0.77,-0.38 -1.54,-0.87 -0.76,-0.38 -0.68,-0.58 -0.67,-0.67 -0.38,-0.48 -0.68,-0.77 -0.57,-0.67 -0.48,-0.77 -0.39,-0.67 -0.38,-0.87 -0.29,-0.76 -0.29,-0.87 -0.19,-0.86 -0.19,-0.77 -0.19,-0.87 -0.29,-2.59 v -1.85 l 0.09,-0.87 v -0.86 l 0.58,-2.6 0.33,-0.77 0.19,-0.87 0.29,-0.86 0.19,-0.87 0.29,-0.86 0.38,-1.06 0.48,-1.15 0.48,-0.87 0.58,-1 0.67,-1 0.39,-0.87 0.48,-0.67 0.57,-0.67 1.44,-1.54 1.45,-1.44 0.76,-0.67 0.87,-0.67 1,-0.68 0.86,-0.67 1,-0.57 1,-0.48 0.87,-0.39 1.25,-0.58 2,-0.67 1.73,-0.38 1.25,-0.19 h 2.11 l 1.25,0.19 1.54,0.29 1.25,0.28 0.86,0.29 0.87,0.39 0.76,0.38 0.58,0.39 1.35,1.15 0.57,0.67 0.58,1 0.48,1.15 0.29,0.87 0.19,0.86 0.09,0.87 v 1.15 l -0.24,1.21 -0.1,1 -0.29,1.15 -0.38,1.15 -0.38,1 -0.29,0.48 -0.48,0.87 z M 408,91.27 l -1.35,0.58 -1.15,0.57 -0.77,0.48 -1.34,1.16 -0.87,0.86 -0.77,0.87 -0.67,0.76 -0.67,1.06 -0.67,1 -0.48,0.86 -0.58,1.06 -0.38,1.15 -0.39,0.87 -0.29,0.77 -0.09,0.67 0.48,0.48 1.25,0.29 1.92,0.09 1,0.1 h 2.69 l 1,-0.19 0.86,-0.19 1,-0.39 0.58,-0.19 1,-0.48 0.67,-0.48 0.67,-0.58 0.68,-0.67 0.67,-0.58 0.57,-0.76 0.48,-0.68 0.39,-0.77 0.48,-0.86 0.58,-1.73 0.19,-1 0.09,-0.86 -0.09,-1 -0.39,-0.87 -0.57,-0.67 -0.77,-0.58 -0.87,-0.38 -0.86,-0.19 -0.86,-0.1 -1,0.19 z"
class="cls-1"
id="path156-8"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
</g>
<g
id="g176-7"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
<path
d="m 72.42,188.94 v 5.5 c 0,15.85 -4.84,26 -15.63,31 13,5.06 18.05,16.73 18.05,33 v 12.55 c 0,23.77 -12.55,36.54 -36.76,36.54 H 0 V 153.5 h 36.54 c 25.09,0 35.88,11.67 35.88,35.44 z M 24.21,175.51 v 40.72 h 9.47 c 9,0 14.53,-4 14.53,-16.28 v -8.59 c 0,-11 -3.75,-15.85 -12.33,-15.85 z m 0,62.74 v 47.32 h 13.87 c 8.15,0 12.55,-3.74 12.55,-15.18 V 257 c 0,-14.31 -4.62,-18.71 -15.63,-18.71 z"
class="cls-1"
id="path160-8"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 87.39,190.48 c 0,-24.65 13,-38.74 36.76,-38.74 23.76,0 36.76,14.09 36.76,38.74 v 80.13 c 0,24.65 -13,38.74 -36.76,38.74 -23.76,0 -36.76,-14.09 -36.76,-38.74 z m 24.21,81.67 c 0,11 4.84,15.19 12.55,15.19 7.71,0 12.55,-4.19 12.55,-15.19 v -83.21 c 0,-11 -4.85,-15.19 -12.55,-15.19 -7.7,0 -12.55,4.18 -12.55,15.19 z"
class="cls-1"
id="path162-7"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 199.65,153.5 v 118.87 c 0,11 4.85,15 12.55,15 7.7,0 12.55,-4 12.55,-15 V 153.5 h 22.89 v 117.33 c 0,24.65 -12.33,38.74 -36.1,38.74 -23.77,0 -36.1,-14.09 -36.1,-38.74 V 153.5 Z"
class="cls-1"
id="path164-7"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="M 257.11,153.5 H 332 v 22 H 306.63 V 307.59 H 282.42 V 175.51 h -25.31 z"
class="cls-1"
id="path166-4"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 342.51,153.5 h 24.21 v 154.09 h -24.21 z"
class="cls-1"
id="path168-4"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 382.35,190.48 c 0,-24.65 13,-38.74 36.76,-38.74 23.76,0 36.76,14.09 36.76,38.74 v 80.13 c 0,8.58 -1.54,15.84 -4.62,21.57 1.1,2.86 2.86,3.3 6.82,3.3 h 2.21 v 21.57 H 457 c -10.78,0 -17.61,-4 -20.91,-10.56 a 49.64,49.64 0 0 1 -16.95,2.86 c -23.77,0 -36.76,-14.09 -36.76,-38.74 z m 24.22,81.67 c 0,11 4.84,15.19 12.54,15.19 7.7,0 12.55,-4.19 12.55,-15.19 v -83.21 c 0,-11 -4.84,-15.19 -12.55,-15.19 -7.71,0 -12.54,4.18 -12.54,15.19 z"
class="cls-1"
id="path170-9"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 494.62,153.5 v 118.87 c 0,11 4.84,15 12.54,15 7.7,0 12.55,-4 12.55,-15 V 153.5 h 22.89 v 117.33 c 0,24.65 -12.32,38.74 -36.1,38.74 -23.78,0 -36.1,-14.09 -36.1,-38.74 V 153.5 Z"
class="cls-1"
id="path172-1"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 583.33,218.44 h 33.23 v 22 h -33.23 v 45.12 h 41.82 v 22 h -66 V 153.5 h 66 v 22 h -41.82 z"
class="cls-1"
id="path174-5"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
</g>
<g
id="g194-5"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
<path
d="m 232.18,435.63 c 2.18,1.13 3.27,1.67 5.48,2.72 l -1.53,3.24 c -2.26,-1.07 -3.38,-1.62 -5.6,-2.77 l -3.38,6.53 c 2.92,1.51 4.39,2.23 7.35,3.6 l -1.5,3.26 c -4.76,-2.21 -7.12,-3.4 -11.76,-5.95 l 12.09,-22 c 4,2.22 6.08,3.25 10.21,5.16 l -1.5,3.26 c -2.68,-1.24 -4,-1.89 -6.64,-3.25 -1.3,2.47 -1.94,3.72 -3.22,6.2 z"
class="cls-1"
id="path178-5"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 258.19,435.05 c 3.8,1.33 5.09,4.1 3.9,7.94 l -0.23,0.75 c -1.55,-0.48 -2.33,-0.73 -3.87,-1.25 l 0.33,-1 c 0.57,-1.7 0.06,-2.58 -1.21,-3 -1.27,-0.42 -2.22,-0.07 -2.83,1.62 -1.77,4.85 6.11,8.53 4,15.38 -1.19,3.84 -4.2,5.36 -8.62,3.8 -4.42,-1.56 -5.85,-4.62 -4.38,-8.36 l 0.57,-1.44 c 1.62,0.64 2.43,0.95 4.06,1.55 l -0.62,1.68 a 2.4,2.4 0 1 0 4.52,1.59 c 1.65,-4.89 -6.25,-8.81 -3.63,-15.49 1.46,-3.76 4.22,-5.1 8.01,-3.77 z"
class="cls-1"
id="path180-8"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 272.24,439.58 c 4.93,1.25 7.42,1.77 12.41,2.63 -0.24,1.42 -0.37,2.12 -0.61,3.54 -1.73,-0.3 -2.59,-0.46 -4.31,-0.8 l -4.2,21.11 c -1.86,-0.37 -2.78,-0.56 -4.63,-1 l 4.73,-21 c -1.71,-0.39 -2.56,-0.59 -4.27,-1 z"
class="cls-1"
id="path182-2"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 293.27,464.91 -0.4,3.78 c -1.8,-0.19 -2.71,-0.3 -4.51,-0.53 0.2,-1.51 0.29,-2.26 0.49,-3.77 1.76,0.23 2.65,0.33 4.42,0.52 z"
class="cls-1"
id="path184-9"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 325.25,447.42 c -1.32,0.1 -2.11,0.83 -2,2.62 0.07,1.07 0.1,1.61 0.17,2.69 -1.62,0.1 -2.43,0.14 -4.06,0.2 0,-1 -0.05,-1.46 -0.09,-2.43 -0.16,-4 1.83,-6.36 5.82,-6.66 3.99,-0.3 6.32,1.7 6.77,5.69 0.89,7.85 -7.71,11.56 -7.44,15.68 a 2.33,2.33 0 0 0 0.07,0.53 c 3.49,-0.22 5.23,-0.37 8.71,-0.76 0.16,1.43 0.24,2.14 0.39,3.57 -5.41,0.6 -8.13,0.8 -13.57,1 l -0.12,-3.09 c -0.29,-7.38 8.4,-9.16 7.76,-16.38 -0.2,-2.23 -1.09,-2.76 -2.41,-2.66 z"
class="cls-1"
id="path186-8"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 341.75,448.11 c -0.69,-3.95 1.09,-6.56 5,-7.4 3.91,-0.84 6.63,0.82 7.61,4.71 1.28,5.07 1.93,7.6 3.21,12.67 1,3.89 -0.92,6.78 -5.47,7.75 -4.55,0.97 -7.45,-0.9 -8.13,-4.86 -0.88,-5.15 -1.33,-7.72 -2.22,-12.87 z m 6.78,12.28 c 0.35,1.76 1.4,2.25 2.85,1.94 1.45,-0.31 2.2,-1.18 1.8,-2.93 -1.2,-5.29 -1.8,-7.94 -3,-13.23 -0.4,-1.75 -1.37,-2.22 -2.67,-1.95 -1.3,0.27 -2,1.11 -1.64,2.87 z"
class="cls-1"
id="path188-2"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 363.19,439.94 c 3.1,-1 3.31,-2.64 3.36,-4.45 1.05,-0.36 1.58,-0.55 2.63,-0.93 3.45,9.43 5.18,14.15 8.64,23.58 -1.8,0.66 -2.7,1 -4.5,1.58 -2.46,-7.26 -3.68,-10.89 -6.14,-18.15 -1.25,0.43 -1.88,0.64 -3.14,1 z"
class="cls-1"
id="path190-0"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 391.63,443.48 1,2.1 a 2.43,2.43 0 1 0 4.34,-2.17 c -1,-2 -1.52,-3 -2.54,-4.91 -0.82,-1.59 -1.93,-1.78 -3.2,-1.14 -1.27,0.64 -1.79,1.63 -1,3.25 l 0.33,0.68 c -1.55,0.75 -2.33,1.11 -3.89,1.82 -2.11,-5.44 -3.18,-8.15 -5.34,-13.57 4,-1.84 6,-2.83 9.88,-5 0.69,1.26 1,1.89 1.71,3.15 -2.62,1.43 -3.95,2.12 -6.62,3.42 0.93,2.21 1.39,3.32 2.31,5.54 a 4.45,4.45 0 0 1 2.62,-3.36 c 2.88,-1.47 5.3,-0.5 7.08,2.74 L 401,441 c 1.94,3.52 0.89,6.76 -3.36,8.89 -4.25,2.13 -7.45,1 -9.11,-2.64 l -0.86,-1.89 c 1.6,-0.75 2.39,-1.12 3.96,-1.88 z"
class="cls-1"
id="path192-7"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
</g>
</g>
<g
id="g202-4"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
transform="matrix(0.97135514,0,0,0.96186606,8.9536653,8.9521229)">
<g
id="g208-8"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill">
<path
d="M 518.64,127 A 231.92,231.92 0 0 0 493.72,88 l -7.85,6.19 a 222.67,222.67 0 0 1 21.5,32.81 z"
class="cls-1"
id="path204-3"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<path
d="m 507.32,334 q -1.47,2.76 -3,5.47 a 222,222 0 0 1 -75.87,78.67 l 5.27,8.5 A 232.17,232.17 0 0 0 513,344.42 q 2.93,-5.15 5.58,-10.42 z"
class="cls-1"
id="path206-0"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
</g>
<polygon
points="81.35,17.5 144.56,127 167.66,127 115.99,37.5 509.16,37.5 457.49,127 480.58,127 543.8,17.5 "
class="cls-1"
id="polygon198-8"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
<polygon
points="287.17,334 264.08,334 312.57,418 361.07,334 337.98,334 312.57,378 "
class="cls-1"
id="polygon200-5"
style="fill:#570d2d;fill-opacity:1;stroke:#ffffff;stroke-width:1.78359292;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
</g>
</g>
</g>
<metadata
id="metadata4195">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>Hipster</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#111}</style></defs><title>Hipster</title><g><path d="M17.6,17.76H16.38v5h1c.95,0,1.53-.41,1.53-1.71V19.42C18.9,18.26,18.51,17.76,17.6,17.76Z" class="cls-1"/><path d="M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0ZM12.2,20.9H9.8V19.16c0-1.15-.51-1.59-1.32-1.59S7.16,18,7.16,19.16V27.9c0,1.15.51,1.57,1.32,1.57S9.8,29.05,9.8,27.9v-3H8.64V22.61H12.2v5.13c0,2.58-1.29,4.06-3.79,4.06s-3.79-1.48-3.79-4.06V19.33c0-2.59,1.3-4.07,3.79-4.07s3.79,1.48,3.79,4.07Zm7,10.72a5,5,0,0,1-.23-2V27.09c0-1.5-.51-2.06-1.66-2.06h-.88v6.59H13.84V15.45h3.83c2.64,0,3.77,1.22,3.77,3.71v1.28c0,1.66-.53,2.74-1.66,3.28,1.27.53,1.68,1.75,1.68,3.44v2.49a4.68,4.68,0,0,0,.28,2Zm9.63,0-.44-2.94H25.22l-.43,2.94H22.45L25,15.45h3.72l2.59,16.17Zm12,0V20L39,31.62h-2.4L34.72,20.18V31.62H32.5V15.45H36L37.9,26.93l1.74-11.48h3.53V31.62Z" class="cls-1"/><polygon points="25.55 26.49 28 26.49 26.77 18.31 25.55 26.49" class="cls-1"/></g></svg>

After

Width:  |  Height:  |  Size: 1018 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.2 (dc2aeda, 2022-05-15)"
sodipodi:docname="Hipster_NavLogo.svg"
version="1.1"
viewBox="0 0 316.16 60"
data-name="Layer 1"
id="Layer_1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata1078">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Hipster</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:current-layer="g1073"
inkscape:window-maximized="0"
inkscape:window-y="25"
inkscape:window-x="0"
inkscape:cy="-23.534382"
inkscape:cx="158.74607"
inkscape:zoom="2.2520243"
showgrid="false"
id="namedview1076"
inkscape:window-height="622"
inkscape:window-width="1370"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ff54ff"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#ff54ff" />
<defs
id="defs1033">
<style
id="style1031">.cls-1{fill:#4cc8c6}.cls-2{fill:#fff}</style>
</defs>
<title
id="title1035">Hipster</title>
<g
id="g1073">
<g
id="g1041"
style="fill:#fe9a9b;fill-opacity:1">
<path
id="path1037"
class="cls-1"
d="M28.65,5.77A22.07,22.07,0,1,1,6.58,27.84,22.09,22.09,0,0,1,28.65,5.77m0-5.77A27.84,27.84,0,1,0,56.48,27.84,27.83,27.83,0,0,0,28.65,0Z"
style="fill:#fe9a9b;fill-opacity:1" />
<path
id="path1039"
class="cls-1"
d="M47.3,16.15,28.65,48.46,10,16.15H47.3m10-5.77H0L28.65,60,57.29,10.38Z"
style="fill:#fe9a9b;fill-opacity:1" />
</g>
<g
id="g1071"
style="fill:#570d2d;fill-opacity:1">
<path
style="fill:#570d2d;fill-opacity:1"
id="path1043"
class="cls-2"
d="M76.11,20.82c0-5.61,3-8.82,8.38-8.82s8.37,3.21,8.37,8.82V39.08c0,5.61-3,8.82-8.37,8.82s-8.38-3.21-8.38-8.82Zm5.52,18.61c0,2.5,1.1,3.46,2.86,3.46s2.85-1,2.85-3.46v-19c0-2.5-1.1-3.46-2.85-3.46s-2.86,1-2.86,3.46Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1045"
class="cls-2"
d="M101.33,22.08V47.5h-5V12.4h6.92l5.66,21v-21h4.92V47.5H108.2Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1047"
class="cls-2"
d="M117.82,12.4h5.52V42.48h9.08v5h-14.6Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1049"
class="cls-2"
d="M134.82,12.4h5.52V47.5h-5.52Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1051"
class="cls-2"
d="M149.26,22.08V47.5h-5V12.4h6.92l5.66,21v-21h4.91V47.5h-5.66Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1053"
class="cls-2"
d="M171.27,27.19h7.57v5h-7.57V42.48h9.53v5H165.75V12.4H180.8v5h-9.53Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1055"
class="cls-2"
d="M191.22,12.4c5.12,0,6.77,2.56,6.77,7.27v2.26c0,3.91-1.05,6.27-5,6.87,4.16.6,5.72,3.46,5.72,7.52v3.11c0,5-2.11,8.07-7.42,8.07H184.2V12.4Zm-1.35,16c4.86,0,7-1.16,7-6.27V19.72c0-4.11-1.25-6.27-5.67-6.27h-5.91v14.9Zm1.41,18.1c4.61,0,6.31-2.61,6.31-7V36.27c0-5.07-2.4-6.92-7.22-6.92h-5.06v17.1Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1057"
class="cls-2"
d="M202.55,21c0-5.81,2.56-8.87,7.43-8.87s7.52,3.06,7.52,8.87V38.87c0,5.82-2.56,8.88-7.52,8.88s-7.43-3.06-7.43-8.88Zm1.11,17.91c0,5.16,2.15,7.82,6.32,7.82s6.41-2.66,6.41-7.82V21c0-5.17-2.2-7.83-6.41-7.83s-6.32,2.66-6.32,7.83Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1059"
class="cls-2"
d="M222.51,38.87c0,4.22,1.5,7.88,6.17,7.88s6.16-3.66,6.16-7.88V12.4h1.06V38.82c0,4.82-1.91,8.93-7.22,8.93s-7.27-4.11-7.27-8.93V12.4h1.1Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1061"
class="cls-2"
d="M246.63,47.5v-34H239v-1H255.4v1h-7.67V47.5Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1063"
class="cls-2"
d="M259.71,12.4V47.5h-1.1V12.4Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1065"
class="cls-2"
d="M278.66,21V38.87a10.14,10.14,0,0,1-1.9,6.47A3.33,3.33,0,0,0,280,47h.55v1H280a4.16,4.16,0,0,1-3.92-2,7,7,0,0,1-4.91,1.71c-5.36,0-7.47-4-7.47-8.88V21c0-4.91,2.11-8.87,7.47-8.87S278.66,16.11,278.66,21Zm-13.89,0v18c0,4.31,1.71,7.82,6.37,7.82s6.42-3.51,6.42-7.82V21c0-4.32-1.76-7.83-6.42-7.83S264.77,16.66,264.77,21Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1067"
class="cls-2"
d="M285.43,38.87c0,4.22,1.5,7.88,6.17,7.88s6.16-3.66,6.16-7.88V12.4h1.06V38.82c0,4.82-1.91,8.93-7.22,8.93s-7.27-4.11-7.27-8.93V12.4h1.1Z" />
<path
style="fill:#570d2d;fill-opacity:1"
id="path1069"
class="cls-2"
d="M314.21,29.2v1H303.88V46.45h12.28v1H302.78V12.4h13.38v1H303.88V29.2Z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#111}</style></defs><title>Hipster</title><g><path d="M17.13,17.76h-1.2v5.47h1.2c.81,0,1.25-.37,1.25-1.52V19.28C18.38,18.13,17.94,17.76,17.13,17.76Z" class="cls-1"/><path d="M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0ZM20.92,21.54c0,2.59-1.25,4-3.79,4h-1.2v6.08H13.39V15.45h3.74c2.54,0,3.79,1.4,3.79,4Zm3.83,10.08H22.21V15.45h2.54Zm9.89,0H32L28.86,19.9V31.62H26.57V15.45h3.19l2.61,9.68V15.45h2.27Z" class="cls-1"/></g></svg>

After

Width:  |  Height:  |  Size: 539 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#605f64}</style></defs><title>Hipster</title><g><path d="M10,3.6A2.75,2.75,0,1,1,7.25,6.35,2.75,2.75,0,0,1,10,3.6Zm0,13a6.58,6.58,0,0,1-5.49-3c0-1.82,3.66-2.82,5.49-2.82s5.47,1,5.49,2.82A6.58,6.58,0,0,1,10,16.6Z" class="cls-1"/><path d="M10,2a8,8,0,1,1-8,8,8,8,0,0,1,8-8m0-2A10,10,0,1,0,20,10,10,10,0,0,0,10,0Z" class="cls-1"/></g></svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 12 12"><defs><style>.cls-1{fill:#605f64}</style></defs><title>Hipster</title><path d="M8.58,7.55H8l-.19-.19a4.48,4.48,0,1,0-.48.48L7.55,8v.55L11,12l1-1Zm-4.12,0A3.09,3.09,0,1,1,7.55,4.46,3.09,3.09,0,0,1,4.46,7.55Z" class="cls-1"/></svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#111}</style></defs><title>Hipster</title><path d="M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0ZM15.12,17.76H12.47V31.62H9.92V17.76H7.27V15.45h7.85Zm11,13.86H22.79L21.87,23l-.93,8.62H17.45l-1.8-16.17h2.47L19.49,28.2l1.22-12.75h2.45l1.27,12.84,1.32-12.84H28Zm5.61,0H29.23V15.45h2.54Zm9-13.86H38.08V31.62H35.54V17.76H32.88V15.45h7.85Z" class="cls-1"/></svg>

After

Width:  |  Height:  |  Size: 469 B

Some files were not shown because too many files have changed in this diff Show More