diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5bd0e4a..037d543 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,10 @@ { - "name": "Python 3", - "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", + "name": "Go Dev Container", + "image": "mcr.microsoft.com/devcontainers/go:1-1.21-bullseye", "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "latest" + }, "ghcr.io/devcontainers/features/aws-cli:1": {}, "ghcr.io/customink/codespaces-features/sam-cli:1": {} }, @@ -11,8 +14,8 @@ "amazonwebservices.aws-toolkit-vscode", "esbenp.prettier-vscode", "github.vscode-github-actions", - "ms-python.black-formatter", - "ms-python.isort", + "redhat.vscode-yaml", + "ms-vscode.makefile-tools", "kddejong.vscode-cfn-lint", "oderwat.indent-rainbow", "github.copilot", @@ -22,20 +25,12 @@ "terminal.integrated.shell.linux": "/usr/bin/zsh", "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[python]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "ms-python.black-formatter" - }, - "isort.args": [ - "--profile", - "black" - ] + } } } }, "mounts": [ "source=${env:HOME}${env:USERPROFILE}/.aws,target=/home/vscode/.aws,type=bind,consistency=cached" ], - "postCreateCommand": "pip install cfn-lint chalice" + "postCreateCommand": "pip install cfn-lint" } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a5f5bda..64c3fd5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,8 +5,8 @@ version: 2 updates: - - package-ecosystem: "pip" - directory: "/function" + - package-ecosystem: "gomod" + directory: "/" schedule: interval: "weekly" commit-message: diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index d509aa6..49df14f 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -10,20 +10,24 @@ on: permissions: id-token: write contents: read - + jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 environment: production steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install jq uses: dcarbone/install-jq-action@v2.1.0 - - name: Setup Python - uses: actions/setup-python@v5 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: "1.21.x" - name: Setup AWS SAM CLI uses: aws-actions/setup-sam@v2 + with: + use-installer: true - name: Setup AWS Credentials uses: aws-actions/configure-aws-credentials@v4.0.2 with: @@ -35,10 +39,10 @@ jobs: SASHA_TABLE_NAME=` \ aws cloudformation describe-stacks --stack-name sasha \ | jq -r '.Stacks[].Outputs[] | select(.OutputKey == "VlrMatchesTableName" ) | .OutputValue'` - echo "SASHA_TABLE_NAME=$SASHA_TABLE_NAME" >> $GITHUB_ENV + echo "SASHA_TABLE_NAME=$SASHA_TABLE_NAME" >> $GITHUB_ENV - name: Prepare SAM parameters env: - SAM_DEPLOY_STAGE: ${{ vars.SAM_DEPLOY_STAGE }} + APPLICATION_NAME: ${{ env.APPLICATION_NAME }} SASHA_TABLE_NAME: ${{ env.SASHA_TABLE_NAME }} API_DOMAIN_NAME: ${{ secrets.API_DOMAIN_NAME }} CERTIFICATE_ARN: ${{ secrets.CERTIFICATE_ARN }} @@ -47,7 +51,7 @@ jobs: run: | tee params.json << EOF { - "SAMDeployStage": "${SAM_DEPLOY_STAGE}", + "ApplicationName": "${APPLICATION_NAME}", "SashaTableName": "${SASHA_TABLE_NAME}", "APIDomainName": "${API_DOMAIN_NAME}", "CertificateArn": "${CERTIFICATE_ARN}", @@ -56,7 +60,7 @@ jobs: } EOF - name: Build SAM packages - run: sam build --use-container + run: sam build - name: Deploy SAM application env: BUCKET_NAME: ${{ secrets.PROVISIONING_BUCKET_NAME }} diff --git a/.gitignore b/.gitignore index 4a32f9d..a01b199 100644 --- a/.gitignore +++ b/.gitignore @@ -1,138 +1,32 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll *.so +*.dylib -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints +# Test binary, built with `go test -c` +*.test -# IPython -profile_default/ -ipython_config.py +# Output of the go coverage tool, specifically when used with LiteIDE +*.out -# pyenv -.python-version +# Dependency directories (remove the comment below to include it) +# vendor/ -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock +# Go workspace file +go.work +go.work.sum -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments +# env file .env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# aws-chalice -api/.chalice/deployed -api/.chalice/deployments -api/.chalice/config.json # aws-sam .aws-sam samconfig.toml + +# Local binary file +dima diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a0e76ff..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", - "editor.formatOnSave": true - } -} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c22b731 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.DEFAULT_GOAL := build + +fmt: + go fmt +.PHONY: fmt + +lint: fmt + staticcheck +.PHONY: lint + +vet: lint + go vet +.PHONY: vet + +build: vet + go mod tidy + CGO_ENABLED=0 go build +.PHONY: build diff --git a/README.md b/README.md index 5635902..07b8ecd 100644 --- a/README.md +++ b/README.md @@ -39,23 +39,6 @@ $ curl endpoint/api/matches?date=2023-05-28 ## usage -### for quick trial - -- In advance, You may have to deploy [sasha](https://github.com/miztch/sasha), data source of dima. -- You can deploy with [AWS Chalice](https://github.com/aws/chalice) - -```bash -table=YOUR_SASHA_TABLE_NAME -sed -i -e 's/SASHA_DYNAMODB_TABLE_NAME/$table/g' api/.chalice/config_template.json -mv api/.chalice/config_template.json api/.chalice/config.json - -cd api -chalice deploy -curl http://${your-api-endpoint}/api/ -``` - -### for permanent deployment - - You can use AWS SAM template (`template.yaml`) - Custom domain with ACM / Route53 Hostzone is implemented. Please remove it if you don't need. diff --git a/api/.chalice/config_template.json b/api/.chalice/config_template.json deleted file mode 100644 index 5c37793..0000000 --- a/api/.chalice/config_template.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "2.0", - "app_name": "dima", - "stages": { - "dev": { - "api_gateway_stage": "api", - "autogen_policy": true, - "environment_variables": { - "TABLE_NAME": "SASHA_DYNAMODB_TABLE_NAME" - } - } - } -} \ No newline at end of file diff --git a/api/app.py b/api/app.py deleted file mode 100644 index b68b367..0000000 --- a/api/app.py +++ /dev/null @@ -1,39 +0,0 @@ -from chalice import Chalice -from chalicelib import database, date_format, log - -logger = log.getLogger() - -app = Chalice(app_name="dima") - - -@app.route("/") -def index(): - """ - return fixed response. - """ - return {"status": 200, "message": "Remember, bullets hurt."} - - -@app.route("/matches") -def matches(): - """ - return upcoming matches list. - """ - request = app.current_request - logger.debug("Request: {}".format(request.to_dict())) - - # default date for search: today - date = date_format.get_default() - - if request.query_params: - param = request.query_params.get("date") - - # validate date format of query parameter (YYYY-mm-dd) - # if format is valid, overwrite date with parameter - if param and date_format.validate(param): - date = param - - matches = database.query(date) - logger.debug("Return {} items.".format(len(matches))) - - return matches diff --git a/api/chalicelib/__init__.py b/api/chalicelib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/chalicelib/database.py b/api/chalicelib/database.py deleted file mode 100644 index eff0b11..0000000 --- a/api/chalicelib/database.py +++ /dev/null @@ -1,47 +0,0 @@ -import os - -import boto3 -from boto3.dynamodb.conditions import Key -from chalicelib import log - -logger = log.getLogger() - - -def _get_table(): - dynamodb = boto3.resource("dynamodb") - table = dynamodb.Table(os.environ["TABLE_NAME"]) - - return table - - -def query(date): - """ - query DynamoDB table by following condition. - - 'startDate' equals to $date - """ - table = _get_table() - - logger.info("Query table. date: {}".format(date)) - - result = [] - - response = table.query(KeyConditionExpression=Key("startDate").eq(date)) - - items = response.get("Items", []) - for item in items: - item["id"] = int(item["id"]) - item["bestOf"] = int(item["bestOf"]) - result.extend(items) - - while "LastEvaluatedKey" in response: - response = table.query( - KeyConditionExpression=Key("startDate").eq(date), - ExclusiveStartKey=response["LastEvaluatedKey"], - ) - for item in items: - item["id"] = int(item["id"]) - item["bestOf"] = int(item["bestOf"]) - result.extend(items) - - logger.info("Found {} items.".format(len(result))) - return result diff --git a/api/chalicelib/date_format.py b/api/chalicelib/date_format.py deleted file mode 100644 index f1a64a0..0000000 --- a/api/chalicelib/date_format.py +++ /dev/null @@ -1,27 +0,0 @@ -import re -from datetime import datetime - -from chalicelib import log - -logger = log.getLogger() - - -def validate(date): - """ - Validate date format (YYYY-mm-dd). - """ - pattern = "^(?!([02468][1235679]|[13579][01345789])00-02-29)(([0-9]{4}-(01|03|05|07|08|10|12)-(0[1-9]|[12][0-9]|3[01]))|([0-9]{4}-(04|06|09|11)-(0[1-9]|[12][0-9]|30))|([0-9]{4}-02-(0[1-9]|1[0-9]|2[0-8]))|([0-9]{2}([02468][048]|[13579][26])-02-29))$" - - if re.fullmatch(pattern, date) is not None: - logger.debug("Date format is valid. input: {}".format(date)) - return True - else: - logger.debug("Date format is invalid or empty. input: {}".format(date)) - return False - - -def get_default(): - """ - Get the default date for search: today. - """ - return datetime.strftime(datetime.now(), "%Y-%m-%d") diff --git a/api/chalicelib/log.py b/api/chalicelib/log.py deleted file mode 100644 index fde5e53..0000000 --- a/api/chalicelib/log.py +++ /dev/null @@ -1,11 +0,0 @@ -import logging - - -def getLogger(): - """ - init logger. - """ - logger = logging.getLogger() - logger.setLevel(logging.INFO) - - return logger diff --git a/api/openapi.yml b/api/openapi.yml new file mode 100644 index 0000000..6efa281 --- /dev/null +++ b/api/openapi.yml @@ -0,0 +1,43 @@ +openapi: 3.0.0 +info: + title: dima + description: competitive VALORANT match API + version: "1.0" +servers: + - url: dima.mizt.ch + +paths: + /: + get: + responses: + "200": + description: "200 response" + content: {} + x-amazon-apigateway-integration: + type: "aws_proxy" + httpMethod: "POST" + uri: + Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations" + passthroughBehavior: "when_no_match" + contentHandling: "CONVERT_TO_TEXT" + responses: + default: + statusCode: "200" + /matches: + get: + responses: + "200": + description: "200 response" + content: {} + x-amazon-apigateway-integration: + type: "aws_proxy" + httpMethod: "POST" + uri: + Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations" + passthroughBehavior: "when_no_match" + contentHandling: "CONVERT_TO_TEXT" + responses: + default: + statusCode: "200" + +components: {} diff --git a/api/requirements.txt b/api/requirements.txt deleted file mode 100644 index 232e33a..0000000 --- a/api/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -ansicon==1.89.0 -blessed==1.20.0 -boto3==1.26.142 -botocore==1.29.143 -chalice==1.28.0 -click==8.1.3 -colorama==0.4.6 -inquirer==2.10.1 -jinxed==1.2.0 -jmespath==1.0.1 -python-dateutil==2.8.2 -python-editor==1.0.4 -PyYAML==6.0 -readchar==4.0.5 -s3transfer==0.6.1 -six==1.16.0 -typing_extensions==4.6.2 -urllib3==1.26.16 -wcwidth==0.2.6 diff --git a/app.go b/app.go new file mode 100644 index 0000000..9a7420c --- /dev/null +++ b/app.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "log" +) + +type App struct { + Router *Router + DBClient *DynamoDBClient + Handlers *Handlers +} + +func NewApp() *App { + app := &App{} + app.initDBClient() + app.initHandlers() + app.initRouter() + return app +} + +func (a *App) initDBClient() { + var err error + a.DBClient, err = NewDynamoDBClient(context.Background(), getDynamoDBConfig().TableName) + if err != nil { + log.Printf("[error] failed to create DynamoDB client, %v", err) + } +} + +func (a *App) initHandlers() { + a.Handlers = NewHandlers(a.DBClient) +} + +func (a *App) initRouter() { + a.Router = NewRouter(a.Handlers) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..084cb7b --- /dev/null +++ b/config.go @@ -0,0 +1,19 @@ +package main + +import "os" + +type dynamoDBConfig struct { + TableName string + Region *string + EndpointURL *string +} + +func getDynamoDBConfig() dynamoDBConfig { + region := os.Getenv("AWS_REGION") + endpointURL := os.Getenv("DYNAMODB_ENDPOINT_URL") + return dynamoDBConfig{ + TableName: os.Getenv("TABLE_NAME"), + Region: ®ion, + EndpointURL: &endpointURL, + } +} diff --git a/date_validator.go b/date_validator.go new file mode 100644 index 0000000..23173f0 --- /dev/null +++ b/date_validator.go @@ -0,0 +1,27 @@ +package main + +import ( + "time" + + "github.com/go-playground/validator" +) + +type DateString struct { + Value string `validate:"datetime=2006-01-02"` +} + +func dateTimeValidator(fl validator.FieldLevel) bool { + dateStr := fl.Field().String() + layout := fl.Param() + _, err := time.Parse(layout, dateStr) + return err == nil +} + +func IsValidDate(dateStr string) bool { + validate := validator.New() + validate.RegisterValidation("datetime", dateTimeValidator) + + date := DateString{Value: dateStr} + err := validate.Struct(date) + return err == nil +} diff --git a/date_validator_test.go b/date_validator_test.go new file mode 100644 index 0000000..06f6503 --- /dev/null +++ b/date_validator_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "testing" +) + +func TestIsValidDate(t *testing.T) { + tests := []struct { + name string + dateStr string + expected bool + }{ + {"Valid date", "2023-07-09", true}, + {"Invalid date format", "2023/07/09", false}, + {"Invalid date", "2023-13-45", false}, + {"Empty string", "", false}, + {"Non-date string", "hello", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValidDate(tt.dateStr) + if result != tt.expected { + t.Errorf("IsValidDate(%s) = %v; want %v", tt.dateStr, result, tt.expected) + } + }) + } +} diff --git a/dynamodb.go b/dynamodb.go new file mode 100644 index 0000000..ceff0ca --- /dev/null +++ b/dynamodb.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" +) + +type DynamoDBClient struct { + client *dynamodb.Client + tableName string +} + +func NewDynamoDBClient(ctx context.Context, tableName string) (*DynamoDBClient, error) { + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return nil, fmt.Errorf("[error] failed to load configuration, %w", err) + } + client := dynamodb.NewFromConfig(cfg) + return &DynamoDBClient{client: client, tableName: tableName}, nil +} + +func (d *DynamoDBClient) QueryMatchesByStartDate(ctx context.Context, startDate string) (matches []match, err error) { + keyEx := expression.Key("startDate").Equal(expression.Value(startDate)) + expr, err := expression.NewBuilder().WithKeyCondition(keyEx).Build() + if err != nil { + return nil, fmt.Errorf("[error] failed to build expression, %w", err) + } + + queryPaginator := dynamodb.NewQueryPaginator(d.client, &dynamodb.QueryInput{ + TableName: &d.tableName, + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + }) + for queryPaginator.HasMorePages() { + response, err := queryPaginator.NextPage(ctx) + if err != nil { + return nil, fmt.Errorf("[error] failed to query matches by start date, %w", err) + } + + var matchPage []match + err = attributevalue.UnmarshalListOfMaps(response.Items, &matchPage) + if err != nil { + return nil, fmt.Errorf("[error] failed to unmarshal match page, %w", err) + } + + matches = append(matches, matchPage...) + } + return matches, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5fc96ad --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module github.com/miztch/dima + +go 1.21.10 + +require ( + github.com/aws/aws-lambda-go v1.47.0 + github.com/aws/aws-sdk-go-v2/config v1.27.24 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.7 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.29 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.1 + github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 + github.com/go-playground/validator v9.31.0+incompatible + github.com/gorilla/mux v1.8.1 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.24 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect + github.com/aws/smithy-go v1.20.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f084b04 --- /dev/null +++ b/go.sum @@ -0,0 +1,92 @@ +github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= +github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go-v2 v1.30.1 h1:4y/5Dvfrhd1MxRDD77SrfsDaj8kUkkljU7XE83NPV+o= +github.com/aws/aws-sdk-go-v2 v1.30.1/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/aws-sdk-go-v2/config v1.27.24 h1:NM9XicZ5o1CBU/MZaHwFtimRpWx9ohAUAqkG6AqSqPo= +github.com/aws/aws-sdk-go-v2/config v1.27.24/go.mod h1:aXzi6QJTuQRVVusAO8/NxpdTeTyr/wRcybdDtfUwJSs= +github.com/aws/aws-sdk-go-v2/credentials v1.17.24 h1:YclAsrnb1/GTQNt2nzv+756Iw4mF8AOzcDfweWwwm/M= +github.com/aws/aws-sdk-go-v2/credentials v1.17.24/go.mod h1:Hld7tmnAkoBQdTMNYZGzztzKRdA4fCdn9L83LOoigac= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.7 h1:pPhmvNKbgb9l5VHcPmMx9g+FHtRbY+ba2J6GefXQGEI= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.7/go.mod h1:OZU7QRvIYXhKry99PttkDTQyN8yCo8RzYjhIKHdQXoo= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.29 h1:yDORZU3cs4TDIS2TCgURxQSydQ52ZXwTZpUL9eqR5HQ= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.29/go.mod h1:Dh4QkDoVpYffQFAEsGbKcWbiu4ifu+viPSINE5IDc3k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 h1:Aznqksmd6Rfv2HQN9cpqIV/lQRMaIpJkLLaJ1ZI76no= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9/go.mod h1:WQr3MY7AxGNxaqAtsDWn+fBxmd4XvLkzeqQ8P1VM0/w= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 h1:5SAoZ4jYpGH4721ZNoS1znQrhOfZinOhc4XuTXx/nVc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13/go.mod h1:+rdA6ZLpaSeM7tSg/B0IEDinCIBJGmW8rKDFkYpP04g= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 h1:WIijqeaAO7TYFLbhsZmi2rgLEAtWOC1LhxCAVTJlSKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13/go.mod h1:i+kbfa76PQbWw/ULoWnp51EYVWH4ENln76fLQE3lXT8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.1 h1:Szwz1vpZkvfhFMJ0X5uUECgHeUmPAxk1UGqAVs/pARw= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.1/go.mod h1:b4wouGyJlzkr2HAvPrDGgYNp1EtmlXOkzhEOvl0c0FQ= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.1 h1:jfkCLx62YWL6bSOkT7aEDKNAX3OwWomlThCxQNBPvbY= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.1/go.mod h1:dLPiMfhRZhblwOeKqdNde7K9jl/pMuIGCGAwC6vQOIo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.14 h1:X1J0Kd17n1PeXeoArNXlvnKewCyMvhVQh7iNMy6oi3s= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.14/go.mod h1:VYMN7l7dxp6xtQRjqIau6d7QAbmPG+yJ75GtCy70f18= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 h1:I9zMeF107l0rJrpnHpjEiiTSCKYAIw8mALiXcPsGBiA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15/go.mod h1:9xWJ3Q/S6Ojusz1UIkfycgD1mGirJfLLKqq3LPT7WN8= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 h1:p1GahKIjyMDZtiKoIn0/jAj/TkMzfzndDv5+zi2Mhgc= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.1/go.mod h1:/vWdhoIoYA5hYoPZ6fm7Sv4d8701PiG5VKe8/pPJL60= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 h1:ORnrOK0C4WmYV/uYt3koHEWBLYsRDwk2Np+eEoyV4Z0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2/go.mod h1:xyFHA4zGxgYkdD73VeezHt3vSKEG9EmFnGwoKlP00u4= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 h1:+woJ607dllHJQtsnJLi52ycuqHMwlW+Wqm2Ppsfp4nQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.1/go.mod h1:jiNR3JqT15Dm+QWq2SRgh0x0bCNSRP2L25+CqPNpJlQ= +github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= +github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 h1:CJyGEyO1CIwOnXTU40urf0mchf6t3voxpvUDikOU9LY= +github.com/awslabs/aws-lambda-go-api-proxy v0.16.2/go.mod h1:vxxjwBHe/KbgFeNlAP/Tvp4SsVRL3WQamcWRxqVh0z0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +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 v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= +github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..eb0b351 --- /dev/null +++ b/handlers.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "time" +) + +type Handlers struct { + dbClient *DynamoDBClient +} + +func NewHandlers(dbClient *DynamoDBClient) *Handlers { + return &Handlers{dbClient: dbClient} +} + +func (h *Handlers) IndexHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("[info] / handler called") + w.Header().Set("Content-Type", "application/json") + response := map[string]interface{}{ + "status": 200, + "message": "Remember, bullets hurt.", + } + json.NewEncoder(w).Encode(response) +} + +func (h *Handlers) GetMatchesHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("[info] /matches handler called") + startDate := time.Now().Format("2006-01-02") + + if inputDate := r.URL.Query().Get("date"); inputDate != "" { + if IsValidDate(inputDate) { + startDate = inputDate + } + } + + log.Printf("[info] query matches by start date: %s", startDate) + matches, err := h.dbClient.QueryMatchesByStartDate(context.Background(), startDate) + if err != nil { + log.Printf("[error] failed to query matches by start date, %v", err) + http.Error(w, "failed to query matches", http.StatusInternalServerError) + return + } + + log.Printf("[info] found %d matches", len(matches)) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(matches) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..cca9ab9 --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "log" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/awslabs/aws-lambda-go-api-proxy/core" + "github.com/awslabs/aws-lambda-go-api-proxy/gorillamux" +) + +var app *App +var gorillaMuxAdapter *gorillamux.GorillaMuxAdapter + +func init() { + log.Printf("Cold start") + app = NewApp() + gorillaMuxAdapter = gorillamux.New(app.Router.muxRouter) +} + +func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + r, err := gorillaMuxAdapter.ProxyWithContext(ctx, *core.NewSwitchableAPIGatewayRequestV1(&req)) + return *r.Version1(), err +} + +func main() { + lambda.Start(Handler) +} diff --git a/match.go b/match.go new file mode 100644 index 0000000..b5796ad --- /dev/null +++ b/match.go @@ -0,0 +1,17 @@ +package main + +type match struct { + Name string `dynamodbabv:"matchName"` + StartTime string `dynamodbabv:"startTime"` + EventName string `dynamodbabv:"eventName"` + Teams []team `dynamodbabv:"teams"` + StartDate string `dynamodbabv:"startDate"` + PagePath string `dynamodbabv:"pagePath"` + BestOf int `dynamodbabv:"bestOf"` + Id int `dynamodbabv:"id"` + EventCountryFlag string `dynamodbabv:"eventCountryFlag"` +} + +type team struct { + Title string +} diff --git a/router.go b/router.go new file mode 100644 index 0000000..7edba4d --- /dev/null +++ b/router.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/gorilla/mux" +) + +type Router struct { + muxRouter *mux.Router +} + +func NewRouter(h *Handlers) *Router { + r := &Router{muxRouter: mux.NewRouter()} + r.RegisterRoutes(h) + return r +} + +func (r *Router) RegisterRoutes(h *Handlers) { + r.muxRouter.HandleFunc("/", h.IndexHandler).Methods("GET") + r.muxRouter.HandleFunc("/matches", h.GetMatchesHandler).Methods("GET") +} diff --git a/template.yaml b/template.yaml index f6c3353..7d1a300 100644 --- a/template.yaml +++ b/template.yaml @@ -2,14 +2,15 @@ AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: dima +Globals: + Api: + OpenApiVersion: 3.0.2 + Parameters: - SAMDeployStage: - Description: Deployment stage + ApplicationName: + Description: Application name Type: String - AllowedValues: - - dev - - stag - - production + Default: dima SashaTableName: Description: DynamoDB table name of sasha Type: String @@ -28,138 +29,66 @@ Parameters: Default: 30 Resources: - APIHandler: + RestAPI: + Type: AWS::Serverless::Api + Properties: + Name: !Ref ApplicationName + StageName: api + DefinitionBody: + Fn::Transform: + Name: AWS::Include + Parameters: + Location: api/openapi.yml + Domain: + BasePath: / + CertificateArn: !Ref CertificateArn + DomainName: !Ref APIDomainName + EndpointConfiguration: EDGE + Route53: + HostedZoneId: !Ref Route53HostedZoneId + + Function: Type: AWS::Serverless::Function + Metadata: + BuildMethod: go1.x Properties: - CodeUri: api/ - Description: dima api handler - Handler: app.app - Runtime: python3.11 + FunctionName: !Ref ApplicationName + CodeUri: . + Handler: bootstrap + Runtime: provided.al2023 MemorySize: 128 - Timeout: 60 - Tags: - aws-chalice: !Sub version=1.28.0:stage=${SAMDeployStage}:app=dima + Timeout: 30 Tracing: PassThrough Policies: - AmazonDynamoDBFullAccess Environment: Variables: TABLE_NAME: !Ref SashaTableName + Events: + Root: + Type: Api + Properties: + Path: / + Method: GET + RestApiId: !Ref RestAPI + Matches: + Type: Api + Properties: + Path: /matches + Method: GET + RestApiId: !Ref RestAPI - APIHandlerInvokePermission: - Type: AWS::Lambda::Permission + FunctionLogGroup: + Type: AWS::Logs::LogGroup Properties: - Action: lambda:InvokeFunction - FunctionName: !Ref APIHandler - Principal: apigateway.amazonaws.com - SourceArn: !Sub "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${RestAPI}/*" - - ApiGatewayCustomDomain: - Type: AWS::ApiGateway::DomainName - Properties: - CertificateArn: !Ref CertificateArn - DomainName: !Ref APIDomainName - EndpointConfiguration: - Types: - - EDGE - SecurityPolicy: TLS_1_2 - - ApiGatewayCustomDomainMapping: - Type: AWS::ApiGateway::BasePathMapping - Properties: - BasePath: (none) - DomainName: !Ref ApiGatewayCustomDomain - RestApiId: !Ref RestAPI - Stage: api - - DNSRecord: - Type: AWS::Route53::RecordSet - Properties: - HostedZoneId: !Ref Route53HostedZoneId - Name: !Ref ApiGatewayCustomDomain - Type: A - AliasTarget: - DNSName: !GetAtt ApiGatewayCustomDomain.DistributionDomainName - EvaluateTargetHealth: False - HostedZoneId: !GetAtt ApiGatewayCustomDomain.DistributionHostedZoneId - - RestAPI: - Type: AWS::Serverless::Api - Properties: - EndpointConfiguration: - Type: EDGE - StageName: api - DefinitionBody: - swagger: "2.0" - info: - title: dima - version: "1.0" - schemes: - - https - paths: - /: - get: - summary: return fixed response. - consumes: - - application/json - produces: - - application/json - responses: - "200": - description: 200 response - x-amazon-apigateway-integration: - contentHandling: CONVERT_TO_TEXT - httpMethod: POST - passthroughBehavior: when_no_match - responses: - default: - statusCode: "200" - type: aws_proxy - uri: - Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${APIHandler.Arn}/invocations - /matches: - get: - summary: return upcoming matches list. - consumes: - - application/json - produces: - - application/json - responses: - "200": - description: 200 response - x-amazon-apigateway-integration: - contentHandling: CONVERT_TO_TEXT - httpMethod: POST - passthroughBehavior: when_no_match - responses: - default: - statusCode: "200" - type: aws_proxy - uri: - Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${APIHandler.Arn}/invocations - x-amazon-apigateway-binary-media-types: - - application/octet-stream - - application/x-tar - - application/zip - - audio/basic - - audio/ogg - - audio/mp4 - - audio/mpeg - - audio/wav - - audio/webm - - image/png - - image/jpg - - image/jpeg - - image/gif - - video/ogg - - video/mpeg - - video/webm + LogGroupName: !Sub /aws/lambda/${Function} + RetentionInDays: !Ref DaysToRetainLogs Outputs: - APIHandlerArn: - Value: !GetAtt APIHandler.Arn - APIHandlerName: - Value: !Ref APIHandler + FunctionArn: + Value: !GetAtt Function.Arn + FunctionName: + Value: !Ref Function CustomEndpointURL: Value: !Sub "https://${APIDomainName}/" EndpointURL: