diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9728488..bf4f255 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -52,19 +52,28 @@ jobs: steps: - uses: actions/checkout@v2 - name: docker-build - run: docker build -t us.gcr.io/otrego-dev/apiserver:${GITHUB_RUN_NUMBER} . + run: docker build -t us.gcr.io/otrego-dev/apiserver:${GITHUB_RUN_NUMBER} -t us.gcr.io/otrego-prod/apiserver:${GITHUB_RUN_NUMBER} . - name: Login to GCP - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master + uses: google-github-actions/setup-gcloud@master with: - version: '281.0.0' + project_id: ${{ secrets.DEV_GCP_PROJECT_ID }} service_account_key: ${{ secrets.DEV_GCP_SA_KEY }} export_default_credentials: true - name: Log into GCP Registry run: gcloud auth configure-docker - name: upload-build run: docker push us.gcr.io/otrego-dev/apiserver:${GITHUB_RUN_NUMBER} - - + - name: Login to GCP Prod + uses: google-github-actions/setup-gcloud@master + with: + project_id: otrego-prod + service_account_key: ${{ secrets.PROD_GA_SA_KEY }} + export_default_credentials: true + - name: Log into GCP Registry + run: gcloud auth configure-docker + - name: upload-build + run: docker push us.gcr.io/otrego-prod/apiserver:${GITHUB_RUN_NUMBER} + deploy: name: Deploy Dev runs-on: ubuntu-latest @@ -74,23 +83,53 @@ jobs: - uses: actions/checkout@v2 - uses: hashicorp/setup-terraform@v1 with: - terraform_version: 0.12.26 + terraform_version: 0.14.8 - name: Login to GCP - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master + uses: google-github-actions/setup-gcloud@master with: - version: '281.0.0' + project_id: ${{ secrets.GCP_PROJECT_ID }} service_account_key: ${{ secrets.DEV_GCP_SA_KEY }} export_default_credentials: true - name: Terraform Init id: init - run: terraform init -input=false tf/ + run: terraform init -input=false tf/dev - name: Terraform Plan id: plan - run: terraform plan -out=plan.tfplan -var="api_docker_image=us.gcr.io/otrego-dev/apiserver:${GITHUB_RUN_NUMBER}" $GITHUB_WORKSPACE/tf/ + run: terraform plan -out=plan.tfplan -var="api_docker_image=us.gcr.io/otrego-dev/apiserver:${GITHUB_RUN_NUMBER}" $GITHUB_WORKSPACE/tf/dev - name: Terraform Apply id: tf-apply run: terraform apply plan.tfplan + + deploy-prod: + name: Deploy Prod + runs-on: ubuntu-latest + needs: deploy + if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == 'otrego/clamshell' + steps: + - uses: actions/checkout@v2 + - uses: hashicorp/setup-terraform@v1 + with: + terraform_version: 0.14.8 + + - name: Login to GCP + uses: google-github-actions/setup-gcloud@master + with: + project_id: ${{ secrets.PROD_GCP_PROJECT_ID }} + service_account_key: ${{ secrets.PROD_GA_SA_KEY }} + export_default_credentials: true + + - name: Terraform Init + id: init + run: terraform init -input=false tf/prod + + - name: Terraform Plan + id: plan + run: terraform plan -out=plan.tfplan -var="api_docker_image=us.gcr.io/otrego-prod/apiserver:${GITHUB_RUN_NUMBER}" $GITHUB_WORKSPACE/tf/prod + + - name: Terraform Apply + id: tf-apply + run: terraform apply plan.tfplan \ No newline at end of file diff --git a/.gitignore b/.gitignore index 954ede6..990ab67 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,8 @@ tmp # Terraform -tf/.terraform -tf/*.plan +.terraform +*.plan tf/*.state # katago analysis/gtp logs diff --git a/core/board/board.go b/core/board/board.go index 5d62da9..e640eb7 100644 --- a/core/board/board.go +++ b/core/board/board.go @@ -96,8 +96,18 @@ func (b *Board) removeCapturedStones(capturedStones []*point.Point) { // capturedStones returns the captured stones in group containing Point pt. // returns nil if no stones were captured. func (b *Board) capturedStones(pt *point.Point) []*point.Point { - expanded := make(map[point.Point]bool) + stoneGroup, captured := b.getStoneGroup(pt) + if captured { + return stoneGroup + } + return nil +} +// getStoneGroup returns all the points in a stone group containing point pt +// and true if the group is captured +func (b *Board) getStoneGroup(pt *point.Point) ([]*point.Point, bool) { + expanded := make(map[point.Point]bool) + captured := true // current group color c := b.colorAt(pt) @@ -114,8 +124,7 @@ func (b *Board) capturedStones(pt *point.Point) []*point.Point { if !b.inBounds(pt1) { continue } else if b.colorAt(pt1) == color.Empty { - // Liberty has been found, no need to continue search - return nil + captured = false } else if b.colorAt(pt1) == c && !expanded[*pt1] { expanded[*pt1] = true points := b.getNeighbors(pt1) @@ -125,14 +134,14 @@ func (b *Board) capturedStones(pt *point.Point) []*point.Point { } } - // The stones that were captured + // The stones in this group stoneGroup := make([]*point.Point, len(expanded)) i := 0 for key := range expanded { stoneGroup[i] = point.New(key.X(), key.Y()) i++ } - return stoneGroup + return stoneGroup, captured } // inBounds returns true if x and y are in bounds @@ -170,11 +179,27 @@ func (b *Board) getNeighbors(pt *point.Point) []*point.Point { // SetPlacements force-places moves on the go-board, without performing capture // logic. If an illegal board position results, return an error. func (b *Board) SetPlacements(ml move.List) error { + for _, m := range ml { b.setColor(m) } - // TODO(kashomon): Validate we have a valid board position -- i.e., one + + // Validate we have a valid board position -- i.e., one // without captures lying on the board. + explored := make(map[point.Point]bool) + for _, m := range ml { + pt := m.Point() + + if !explored[*pt] { + stoneGroup, captured := b.getStoneGroup(pt) + if captured { + return fmt.Errorf("invalid board state. stones at points %v are captured", stoneGroup) + } + for _, point := range stoneGroup { + explored[*point] = true + } + } + } return nil } diff --git a/core/board/board_test.go b/core/board/board_test.go index e643d37..e06d3cc 100644 --- a/core/board/board_test.go +++ b/core/board/board_test.go @@ -200,6 +200,73 @@ func TestRemoveCapturedStones(t *testing.T) { } } +func TestSetPlacements(t *testing.T) { + testCases := []struct { + desc string + ml move.List + expErrSubstr string + }{ + { + desc: "current placements", + ml: move.List{ + move.New(color.White, point.New(1, 4)), + move.New(color.White, point.New(2, 4)), + move.New(color.White, point.New(3, 4)), + + move.New(color.White, point.New(4, 1)), + move.New(color.White, point.New(4, 2)), + move.New(color.White, point.New(4, 3)), + + move.New(color.White, point.New(4, 5)), + move.New(color.White, point.New(4, 6)), + move.New(color.White, point.New(4, 7)), + + move.New(color.White, point.New(5, 4)), + move.New(color.White, point.New(6, 4)), + move.New(color.White, point.New(7, 4)), + }, + }, + { + desc: "current placements", + ml: move.List{ + move.New(color.White, point.New(1, 4)), + move.New(color.White, point.New(2, 4)), + move.New(color.White, point.New(3, 4)), + + move.New(color.White, point.New(4, 1)), + move.New(color.White, point.New(4, 2)), + move.New(color.White, point.New(4, 3)), + + move.New(color.White, point.New(4, 5)), + move.New(color.White, point.New(4, 6)), + move.New(color.White, point.New(4, 7)), + + move.New(color.White, point.New(5, 4)), + move.New(color.White, point.New(6, 4)), + move.New(color.White, point.New(7, 4)), + + move.New(color.Black, point.New(4, 4)), + }, + expErrSubstr: "invalid board state", + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + b := New(19) + err := b.SetPlacements(tc.ml) + + cerr := errcheck.CheckCases(err, tc.expErrSubstr) + if cerr != nil { + t.Error(cerr) + return + } + if err != nil { + return + } + + }) + } +} func TestPlaceStone(t *testing.T) { testCases := []struct { desc string diff --git a/core/point/point.go b/core/point/point.go index 224329e..eac8049 100644 --- a/core/point/point.go +++ b/core/point/point.go @@ -2,6 +2,7 @@ package point import ( + "encoding/json" "fmt" ) @@ -42,21 +43,27 @@ func (pt *Point) String() string { return fmt.Sprintf("{%d,%d}", pt.x, pt.y) } -// Key is a convenience helper to convert this point to a key-struct. -func (pt *Point) Key() Key { - return Key{X: pt.X(), Y: pt.Y()} +// pointInternal is an internal struct for the purposes of JSON Conversion. +type pointInternal struct { + X int `json:"x"` + Y int `json:"y"` } -// Key is a point-struct that is used for keys in maps. As such, it's intended -// to be used like the following: -// -// Key{X:12, Y:15} -type Key struct { - X int - Y int +// MarshalJSON indicates to the JSON library how to marshal a Point. +func (pt *Point) MarshalJSON() ([]byte, error) { + return json.Marshal(&pointInternal{ + X: pt.x, + Y: pt.y, + }) } -// Point converts a point-Key back to a point. -func (k Key) Point() *Point { - return New(k.X, k.Y) +// UnmarshalJSON indicates to the JSON library how to unmarshal a Point. +func (pt *Point) UnmarshalJSON(data []byte) error { + var pti pointInternal + if err := json.Unmarshal(data, &pti); err != nil { + return err + } + pt.x = pti.X + pt.y = pti.Y + return nil } diff --git a/core/point/point_test.go b/core/point/point_test.go index 0735acd..f0f8ed6 100644 --- a/core/point/point_test.go +++ b/core/point/point_test.go @@ -1,6 +1,7 @@ package point import ( + "encoding/json" "fmt" "testing" ) @@ -142,18 +143,17 @@ func TestSGFToPointTranslate(t *testing.T) { } } -func TestKey(t *testing.T) { - pt := New(12, 15) - - key := pt.Key() - exp := Key{X: 12, Y: 15} - - if key != exp { - t.Errorf("error converting point to key: got %v, but expected %v", key, exp) +func TestJSON(t *testing.T) { + pt := New(1, 2) + by, err := json.Marshal(pt) + if err != nil { + t.Fatal(err) } - - back := exp.Point() - if !pt.Equal(back) { - t.Errorf("error converting key to point: got %v, but expected %v", back, pt) + var back Point + if err := json.Unmarshal(by, &back); err != nil { + t.Fatal(err) + } + if !back.Equal(pt) { + t.Fatalf("got point %v, but expected point %v", back, pt) } } diff --git a/tf/.terraform.lock.hcl b/tf/.terraform.lock.hcl new file mode 100644 index 0000000..bda5225 --- /dev/null +++ b/tf/.terraform.lock.hcl @@ -0,0 +1,20 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "3.59.0" + constraints = "3.59.0" + hashes = [ + "h1:rubiy+932DM9kWMJSz5u8zgUGj7Iza6m5krr0FuPi9E=", + "zh:1210d60719470b32d979390a73fa2405ceb9702f2728854cac3c3804bf774442", + "zh:1e0cec25c527cd09d94ddcea55522e3d75a600745f3d8cd46296e610dde41abf", + "zh:3eff1094a52a680d044ed8182ca1b70a8a509e4200fd89deae220b21503832a2", + "zh:604c5fdb7d15268e4a5210cfcc5630f34c9a0a06d8ef5f6f3a93513aad278e11", + "zh:6c02ff804cfa2fd7dda4c090f06ee999ce6fed2bc7fe408fa3ba312d57b64d56", + "zh:8954c3691d665f44ed7bda1c7f5d02f4980698657b6518b4445842f80c146481", + "zh:8e1f53a315341285b04aa50dda086be1f84d02ab92a9f4a3875e648374829a7b", + "zh:e0b1f047f65a8403ea16157d4f3f8492d4b23ceab85b939f2bcd368e2d8f0252", + "zh:f795a80a734d7730fe0b876f16705964a80bd155925aecc60026c0e8dab145ca", + "zh:ffdcdebaabc34467db790a8c3e769fa6e44f580e4a162de1ad4f7156e54064fd", + ] +} diff --git a/tf/README.md b/tf/README.md index 1816b9a..77e98e5 100644 --- a/tf/README.md +++ b/tf/README.md @@ -4,8 +4,11 @@ Our infrastructure is managed with Terraform. ## Install terraform. Note: version 0.12 is required at this time. -`brew install terraform` +`brew install tfenv` `brew install google-cloud-sdk` +`tfenv install 0.14.8` +`tfenv use 0.14.8` +`brew install tflint` ## Credentials diff --git a/tf/main.tf b/tf/api_server_mod/main.tf similarity index 81% rename from tf/main.tf rename to tf/api_server_mod/main.tf index efc8175..a9aa745 100644 --- a/tf/main.tf +++ b/tf/api_server_mod/main.tf @@ -1,17 +1,8 @@ -terraform { - backend "gcs" { - bucket = "otrego-dev-infrastructure" - prefix = "terraform/dev/api" - } - # Fix terraform to version 0.12.x - required_version = ">= 0.12, < 0.13, < 1.0" -} - provider "google" { } locals { - instance_name = format("%s-%s", var.instance_name, substr(md5(module.gce-container.container.image), 0, 8)) + instance_name = format("%s-container-%s", var.project_id, substr(md5(module.gce-container.container.image), 0, 8)) } module "gce-container" { @@ -20,9 +11,9 @@ module "gce-container" { container = { image = var.api_docker_image env = [ - { - name = "TEST_VAR" - value = "Hello World!" + { + name = "OTREGO_PORT", + value = "80" } ], @@ -90,7 +81,7 @@ resource "google_compute_firewall" "default" { allow { protocol = "tcp" - ports = ["8080"] + ports = ["8080", "80"] } direction = "INGRESS" @@ -100,7 +91,7 @@ resource "google_compute_firewall" "default" { } resource "google_dns_record_set" "frontend" { - name = "dev.otrego.com." + name = var.dns_name type = "A" ttl = 300 diff --git a/tf/outputs.tf b/tf/api_server_mod/outputs.tf similarity index 93% rename from tf/outputs.tf rename to tf/api_server_mod/outputs.tf index 4bf03df..14db03c 100644 --- a/tf/outputs.tf +++ b/tf/api_server_mod/outputs.tf @@ -24,6 +24,6 @@ output "ipv4" { } output "cos_image_name" { - description = "The cos image used" + description = "The conatiner optimized image used" value = var.cos_image_name } diff --git a/tf/variables.tf b/tf/api_server_mod/variables.tf similarity index 71% rename from tf/variables.tf rename to tf/api_server_mod/variables.tf index 4e40cb3..64a0c1b 100644 --- a/tf/variables.tf +++ b/tf/api_server_mod/variables.tf @@ -1,6 +1,8 @@ variable "dns_managed_zone" { - description = "The dns managed zone in GCP Cloud DNS. This is where DNS entries should be writable." - default = "otrego-dev" + description = "The dns managed zone in GCP Cloud DNS. This is where DNS entries should be writable. Example: otrego-dev" +} +variable "dns_name" { + description = "A record entry within the DNS Manged Zone. Eg: dev.otrego.com." } variable "project_id" { @@ -18,11 +20,6 @@ variable "subnetwork" { default = "default" } -variable "instance_name" { - description = "The desired name to assign to the deployed instance" - default = "hello-world-container-vm" -} - variable "zone" { description = "The GCP zone to deploy instances into" type = string @@ -36,8 +33,7 @@ variable "client_email" { } variable "cos_image_name" { - description = "The forced COS image to use instead of latest" - default = "cos-stable-77-12371-89-0" + description = "The container optimized image. This is the base image for the machine on which docker is run. Should be updated regularly. Example: cos-stable-77-12371-89-0" } variable "api_docker_image" { diff --git a/tf/dev/.terraform.lock.hcl b/tf/dev/.terraform.lock.hcl new file mode 100644 index 0000000..bda5225 --- /dev/null +++ b/tf/dev/.terraform.lock.hcl @@ -0,0 +1,20 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "3.59.0" + constraints = "3.59.0" + hashes = [ + "h1:rubiy+932DM9kWMJSz5u8zgUGj7Iza6m5krr0FuPi9E=", + "zh:1210d60719470b32d979390a73fa2405ceb9702f2728854cac3c3804bf774442", + "zh:1e0cec25c527cd09d94ddcea55522e3d75a600745f3d8cd46296e610dde41abf", + "zh:3eff1094a52a680d044ed8182ca1b70a8a509e4200fd89deae220b21503832a2", + "zh:604c5fdb7d15268e4a5210cfcc5630f34c9a0a06d8ef5f6f3a93513aad278e11", + "zh:6c02ff804cfa2fd7dda4c090f06ee999ce6fed2bc7fe408fa3ba312d57b64d56", + "zh:8954c3691d665f44ed7bda1c7f5d02f4980698657b6518b4445842f80c146481", + "zh:8e1f53a315341285b04aa50dda086be1f84d02ab92a9f4a3875e648374829a7b", + "zh:e0b1f047f65a8403ea16157d4f3f8492d4b23ceab85b939f2bcd368e2d8f0252", + "zh:f795a80a734d7730fe0b876f16705964a80bd155925aecc60026c0e8dab145ca", + "zh:ffdcdebaabc34467db790a8c3e769fa6e44f580e4a162de1ad4f7156e54064fd", + ] +} diff --git a/tf/dev/main.tf b/tf/dev/main.tf new file mode 100644 index 0000000..579682e --- /dev/null +++ b/tf/dev/main.tf @@ -0,0 +1,16 @@ +terraform { + backend "gcs" { + bucket = "otrego-dev-infrastructure" + prefix = "terraform/dev/api" + } +} + +module "api_server_mod" { + source = "../api_server_mod" + dns_managed_zone = "otrego-dev" + dns_name = "dev.otrego.com." + project_id = "otrego-dev" + subnetwork_project = "otrego-dev" + api_docker_image = var.api_docker_image + cos_image_name = "cos-stable-85-13310-1209-17" +} \ No newline at end of file diff --git a/tf/dev/outputs.tf b/tf/dev/outputs.tf new file mode 100644 index 0000000..d115f7d --- /dev/null +++ b/tf/dev/outputs.tf @@ -0,0 +1,26 @@ +output "vm_container_label" { + description = "The instance label containing container configuration" + value = module.api_server_mod.vm_container_label +} + +output "container" { + description = "The container metadata provided to the module" + value = module.api_server_mod.container +} + +output "volumes" { + description = "The volume metadata provided to the module" + value = module.api_server_mod.volumes +} + +output "instance_name" { + description = "The deployed instance name" + value = module.api_server_mod.instance_name +} + +output "ipv4" { + description = "The public IP address of the deployed instance" + value = module.api_server_mod.ipv4 +} + + diff --git a/tf/dev/variables.tf b/tf/dev/variables.tf new file mode 100644 index 0000000..013168f --- /dev/null +++ b/tf/dev/variables.tf @@ -0,0 +1,5 @@ + +variable "api_docker_image" { + description = "Docker Image of the Otrego API. Example: us.gcr.io/otrego-dev/apiserver:393" + type = string +} \ No newline at end of file diff --git a/tf/dev/versions.tf b/tf/dev/versions.tf new file mode 100644 index 0000000..2f55554 --- /dev/null +++ b/tf/dev/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "3.59.0" + } + } + required_version = ">= 0.14, < 0.15, < 1.0" +} diff --git a/tf/prod/main.tf b/tf/prod/main.tf new file mode 100644 index 0000000..84ec321 --- /dev/null +++ b/tf/prod/main.tf @@ -0,0 +1,16 @@ +terraform { + backend "gcs" { + bucket = "otrego-prod-infrastructure" + prefix = "terraform/prod/api" + } +} + +module "api_server_mod" { + source = "../api_server_mod" + dns_managed_zone = "otrego-prod" + dns_name = "www.otrego.com." + project_id = "otrego-prod" + subnetwork_project = "otrego-prod" + api_docker_image = var.api_docker_image + cos_image_name = "cos-stable-85-13310-1209-17" +} \ No newline at end of file diff --git a/tf/prod/outputs.tf b/tf/prod/outputs.tf new file mode 100644 index 0000000..d115f7d --- /dev/null +++ b/tf/prod/outputs.tf @@ -0,0 +1,26 @@ +output "vm_container_label" { + description = "The instance label containing container configuration" + value = module.api_server_mod.vm_container_label +} + +output "container" { + description = "The container metadata provided to the module" + value = module.api_server_mod.container +} + +output "volumes" { + description = "The volume metadata provided to the module" + value = module.api_server_mod.volumes +} + +output "instance_name" { + description = "The deployed instance name" + value = module.api_server_mod.instance_name +} + +output "ipv4" { + description = "The public IP address of the deployed instance" + value = module.api_server_mod.ipv4 +} + + diff --git a/tf/prod/variables.tf b/tf/prod/variables.tf new file mode 100644 index 0000000..013168f --- /dev/null +++ b/tf/prod/variables.tf @@ -0,0 +1,5 @@ + +variable "api_docker_image" { + description = "Docker Image of the Otrego API. Example: us.gcr.io/otrego-dev/apiserver:393" + type = string +} \ No newline at end of file diff --git a/tf/prod/versions.tf b/tf/prod/versions.tf new file mode 100644 index 0000000..2f55554 --- /dev/null +++ b/tf/prod/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "3.59.0" + } + } + required_version = ">= 0.14, < 0.15, < 1.0" +}