398 lines
10 KiB
Go
398 lines
10 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Command gitops-pusher allows users to use a GitOps flow for managing Tailscale ACLs.
|
|
//
|
|
// See README.md for more details.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
"github.com/tailscale/hujson"
|
|
"golang.org/x/oauth2/clientcredentials"
|
|
"tailscale.com/client/tailscale"
|
|
"tailscale.com/util/httpm"
|
|
)
|
|
|
|
var (
|
|
rootFlagSet = flag.NewFlagSet("gitops-pusher", flag.ExitOnError)
|
|
policyFname = rootFlagSet.String("policy-file", "./policy.hujson", "filename for policy file")
|
|
cacheFname = rootFlagSet.String("cache-file", "./version-cache.json", "filename for the previous known version hash")
|
|
timeout = rootFlagSet.Duration("timeout", 5*time.Minute, "timeout for the entire CI run")
|
|
githubSyntax = rootFlagSet.Bool("github-syntax", true, "use GitHub Action error syntax (https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message)")
|
|
apiServer = rootFlagSet.String("api-server", "api.tailscale.com", "API server to contact")
|
|
)
|
|
|
|
func modifiedExternallyError() {
|
|
if *githubSyntax {
|
|
fmt.Printf("::warning file=%s,line=1,col=1,title=Policy File Modified Externally::The policy file was modified externally in the admin console.\n", *policyFname)
|
|
} else {
|
|
fmt.Printf("The policy file was modified externally in the admin console.\n")
|
|
}
|
|
}
|
|
|
|
func apply(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
|
|
return func(ctx context.Context, args []string) error {
|
|
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
localEtag, err := sumFile(*policyFname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cache.PrevETag == "" {
|
|
log.Println("no previous etag found, assuming local file is correct and recording that")
|
|
cache.PrevETag = localEtag
|
|
}
|
|
|
|
log.Printf("control: %s", controlEtag)
|
|
log.Printf("local: %s", localEtag)
|
|
log.Printf("cache: %s", cache.PrevETag)
|
|
|
|
if cache.PrevETag != controlEtag {
|
|
modifiedExternallyError()
|
|
}
|
|
|
|
if controlEtag == localEtag {
|
|
cache.PrevETag = localEtag
|
|
log.Println("no update needed, doing nothing")
|
|
return nil
|
|
}
|
|
|
|
if err := applyNewACL(ctx, client, tailnet, apiKey, *policyFname, controlEtag); err != nil {
|
|
return err
|
|
}
|
|
|
|
cache.PrevETag = localEtag
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func test(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
|
|
return func(ctx context.Context, args []string) error {
|
|
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
localEtag, err := sumFile(*policyFname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cache.PrevETag == "" {
|
|
log.Println("no previous etag found, assuming local file is correct and recording that")
|
|
cache.PrevETag = localEtag
|
|
}
|
|
|
|
log.Printf("control: %s", controlEtag)
|
|
log.Printf("local: %s", localEtag)
|
|
log.Printf("cache: %s", cache.PrevETag)
|
|
|
|
if cache.PrevETag != controlEtag {
|
|
modifiedExternallyError()
|
|
}
|
|
|
|
if controlEtag == localEtag {
|
|
log.Println("no updates found, doing nothing")
|
|
return nil
|
|
}
|
|
|
|
if err := testNewACLs(ctx, client, tailnet, apiKey, *policyFname); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func getChecksums(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
|
|
return func(ctx context.Context, args []string) error {
|
|
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
localEtag, err := sumFile(*policyFname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cache.PrevETag == "" {
|
|
log.Println("no previous etag found, assuming local file is correct and recording that")
|
|
cache.PrevETag = Shuck(localEtag)
|
|
}
|
|
|
|
log.Printf("control: %s", controlEtag)
|
|
log.Printf("local: %s", localEtag)
|
|
log.Printf("cache: %s", cache.PrevETag)
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
tailnet, ok := os.LookupEnv("TS_TAILNET")
|
|
if !ok {
|
|
log.Fatal("set envvar TS_TAILNET to your tailnet's name")
|
|
}
|
|
apiKey, ok := os.LookupEnv("TS_API_KEY")
|
|
oauthId, oiok := os.LookupEnv("TS_OAUTH_ID")
|
|
oauthSecret, osok := os.LookupEnv(" |