From 4ec5b7fc6955a14189d39bc67f1b61b3602e04c8 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 5 Jun 2020 18:45:31 +0300 Subject: [PATCH] Implement GitHub repository bootstrap --- cmd/tk/bootstrap.go | 20 ++ cmd/tk/bootstrap_github.go | 364 +++++++++++++++++++++++++++++++++++++ cmd/tk/utils.go | 15 ++ go.sum | 13 ++ 4 files changed, 412 insertions(+) create mode 100644 cmd/tk/bootstrap.go create mode 100644 cmd/tk/bootstrap_github.go diff --git a/cmd/tk/bootstrap.go b/cmd/tk/bootstrap.go new file mode 100644 index 00000000..052186c6 --- /dev/null +++ b/cmd/tk/bootstrap.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var bootstrapCmd = &cobra.Command{ + Use: "bootstrap", + Short: "Bootstrap commands", +} + +var ( + bootstrapVersion string +) + +func init() { + bootstrapCmd.PersistentFlags().StringVar(&bootstrapVersion, "version", "master", "toolkit tag or branch") + + rootCmd.AddCommand(bootstrapCmd) +} diff --git a/cmd/tk/bootstrap_github.go b/cmd/tk/bootstrap_github.go new file mode 100644 index 00000000..ce91202b --- /dev/null +++ b/cmd/tk/bootstrap_github.go @@ -0,0 +1,364 @@ +package main + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "sigs.k8s.io/yaml" + "strings" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/google/go-github/v32/github" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1" + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" +) + +var bootstrapGitHubCmd = &cobra.Command{ + Use: "github", + Short: "Bootstrap GitHub repository", + RunE: bootstrapGitHubCmdRun, +} + +var ( + ghOwner string + ghRepository string +) + +const ghTokenName = "GITHUB_TOKEN" + +func init() { + bootstrapGitHubCmd.Flags().StringVar(&ghOwner, "owner", "", "GitHub user or organization name") + bootstrapGitHubCmd.Flags().StringVar(&ghRepository, "repository", "", "GitHub repository name") + bootstrapCmd.AddCommand(bootstrapGitHubCmd) +} + +func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { + ghToken := os.Getenv(ghTokenName) + ghURL := fmt.Sprintf("https://github.com/%s/%s", ghOwner, ghRepository) + if ghToken == "" { + return fmt.Errorf("%s environment variable not found", ghTokenName) + } + + if ghOwner == "" || ghRepository == "" { + return fmt.Errorf("owner and repository are required") + } + + tmpDir, err := ioutil.TempDir("", namespace) + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // create GitHub repository if doesn't exists + logAction("connecting to GitHub") + if err := initGitHubRepository(ctx, ghOwner, ghRepository, ghToken); err != nil { + return err + } + + // clone repository and checkout the master branch + repo, err := checkoutGitHubRepository(ctx, tmpDir, ghURL, ghToken) + if err != nil { + return err + } + w, err := repo.Worktree() + if err != nil { + return err + } + logSuccess("repository cloned") + + // generate install manifests + logGenerate("generating manifests") + kDir := path.Join(tmpDir, ".kustomization") + if err := os.MkdirAll(kDir, os.ModePerm); err != nil { + return fmt.Errorf("generating manifests failed: %w", err) + } + if err := genInstallManifests(bootstrapVersion, namespace, components, kDir); err != nil { + return fmt.Errorf("generating manifests failed: %w", err) + } + + manifestsDir := path.Join(tmpDir, namespace) + if err := os.MkdirAll(manifestsDir, os.ModePerm); err != nil { + return fmt.Errorf("generating manifests failed: %w", err) + } + + manifest := path.Join(manifestsDir, "toolkit.yaml") + if err := buildKustomization(kDir, manifest); err != nil { + return fmt.Errorf("build kustomization failed: %w", err) + } + + os.RemoveAll(kDir) + + // stage install manifests + _, err = w.Add(fmt.Sprintf("%s/toolkit.yaml", namespace)) + if err != nil { + return err + } + + status, err := w.Status() + if err != nil { + return err + } + + isInstall := !status.IsClean() + + if isInstall { + // commit install manifests + logGenerate("pushing manifests") + _, err = w.Commit("Add bootstrap manifests", &git.CommitOptions{ + Author: &object.Signature{ + Name: "tk", + Email: "tk@@users.noreply.github.com", + When: time.Now(), + }, + }) + if err != nil { + return err + } + + // push install manifests + if err := pushGitHubRepository(ctx, repo, ghToken); err != nil { + return err + } + logSuccess("manifests pushed") + + // apply install manifests + logAction("installing components in %s namespace", namespace) + command := fmt.Sprintf("cat %s | kubectl apply -f-", manifest) + if _, err := utils.execCommand(ctx, ModeOS, command); err != nil { + return fmt.Errorf("install failed") + } + logSuccess("install completed") + + // check rollout status + logWaiting("verifying installation") + for _, deployment := range components { + command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=%s", + namespace, deployment, timeout.String()) + if _, err := utils.execCommand(ctx, ModeOS, command); err != nil { + return fmt.Errorf("install failed") + } else { + logSuccess("%s ready", deployment) + } + } + } + + // create or update auth secret + if err := generateBasicAuth(ctx, namespace, namespace, "git", ghToken); err != nil { + return err + } + logSuccess("authentication configured") + + // generate and push source kustomization + if isInstall { + logAction("generating kustomization manifests") + if err := generateGitHubKustomization(ghURL, namespace, namespace, tmpDir); err != nil { + return err + } + + // stage manifests + _, err = w.Add(fmt.Sprintf("%s", namespace)) + if err != nil { + return err + } + + // commit manifests + logAction("pushing kustomization manifests") + _, err = w.Commit("Add kustomization manifests", &git.CommitOptions{ + Author: &object.Signature{ + Name: "tk", + Email: "tk@@users.noreply.github.com", + When: time.Now(), + }, + }) + if err != nil { + return err + } + + // push install manifests + if err := pushGitHubRepository(ctx, repo, ghToken); err != nil { + return err + } + logSuccess("kustomization manifests pushed") + + logAction("applying kustomization manifests") + if err := applyGitHubKustomization(ctx, namespace, namespace, tmpDir); err != nil { + return err + } + } + + logSuccess("bootstrap finished") + return nil +} + +func initGitHubRepository(ctx context.Context, owner, name, token string) error { + isPrivate := true + isAutoInit := true + auth := github.BasicAuthTransport{ + Username: "git", + Password: token, + } + + client := github.NewClient(auth.Client()) + _, _, err := client.Repositories.Create(ctx, owner, &github.Repository{ + AutoInit: &isAutoInit, + Name: &name, + Private: &isPrivate, + }) + if err != nil { + if !strings.Contains(err.Error(), "name already exists on this account") { + return fmt.Errorf("github create repository error: %w", err) + } + } else { + logSuccess("repository created") + } + return nil +} + +func checkoutGitHubRepository(ctx context.Context, path, url, token string) (*git.Repository, error) { + branch := "master" + auth := &http.BasicAuth{ + Username: "git", + Password: token, + } + repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{ + URL: url, + Auth: auth, + RemoteName: git.DefaultRemoteName, + ReferenceName: plumbing.NewBranchReferenceName(branch), + SingleBranch: true, + NoCheckout: false, + Progress: nil, + Tags: git.NoTags, + }) + if err != nil { + return nil, fmt.Errorf("git clone error: %w", err) + } + + _, err = repo.Head() + if err != nil { + return nil, fmt.Errorf("git resolve HEAD error: %w", err) + } + + return repo, nil +} + +func pushGitHubRepository(ctx context.Context, repo *git.Repository, token string) error { + auth := &http.BasicAuth{ + Username: "git", + Password: token, + } + err := repo.PushContext(ctx, &git.PushOptions{ + Auth: auth, + Progress: nil, + }) + if err != nil { + return fmt.Errorf("git push error: %w", err) + } + return nil +} + +func generateGitHubKustomization(url, name, namespace, tmpDir string) error { + gvk := sourcev1.GroupVersion.WithKind("GitRepository") + gitRepository := sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: gvk.Kind, + APIVersion: gvk.GroupVersion().String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: sourcev1.GitRepositorySpec{ + URL: url, + Interval: metav1.Duration{ + Duration: 1 * time.Minute, + }, + Reference: &sourcev1.GitRepositoryRef{ + Branch: "master", + }, + SecretRef: &corev1.LocalObjectReference{ + Name: name, + }, + }, + } + + gitData, err := yaml.Marshal(gitRepository) + if err != nil { + return err + } + + if err := utils.writeFile(string(gitData), filepath.Join(tmpDir, namespace, "toolkit-source.yaml")); err != nil { + return err + } + + gvk = kustomizev1.GroupVersion.WithKind("Kustomization") + emptyAPIGroup := "" + kustomization := kustomizev1.Kustomization{ + TypeMeta: metav1.TypeMeta{ + Kind: gvk.Kind, + APIVersion: gvk.GroupVersion().String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{ + Duration: 10 * time.Minute, + }, + Path: "./", + Prune: true, + SourceRef: corev1.TypedLocalObjectReference{ + APIGroup: &emptyAPIGroup, + Kind: "GitRepository", + Name: name, + }, + }, + } + + ksData, err := yaml.Marshal(kustomization) + if err != nil { + return err + } + + if err := utils.writeFile(string(ksData), filepath.Join(tmpDir, namespace, "toolkit-kustomization.yaml")); err != nil { + return err + } + + return nil +} + +func applyGitHubKustomization(ctx context.Context, name, namespace, tmpDir string) error { + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + command := fmt.Sprintf("kubectl apply -f %s", filepath.Join(tmpDir, namespace)) + if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil { + return err + } + + logWaiting("waiting for kustomization sync") + if err := wait.PollImmediate(pollInterval, timeout, + isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil { + return err + } + + return nil +} diff --git a/cmd/tk/utils.go b/cmd/tk/utils.go index 41ae679f..aec69352 100644 --- a/cmd/tk/utils.go +++ b/cmd/tk/utils.go @@ -112,3 +112,18 @@ func (*Utils) kubeClient(config string) (client.Client, error) { return kubeClient, nil } + +func (*Utils) writeFile(content, filename string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + _, err = io.WriteString(file, content) + if err != nil { + return err + } + + return file.Sync() +} diff --git a/go.sum b/go.sum index 5291ba85..7d9cdf05 100644 --- a/go.sum +++ b/go.sum @@ -48,13 +48,16 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= @@ -169,6 +172,7 @@ github.com/fluxcd/source-controller v0.0.1-alpha.6 h1:CpSH1mA9bRjifRwmcVS98ejY2M github.com/fluxcd/source-controller v0.0.1-alpha.6/go.mod h1:gvU+sR6yH9kbyWZAJ9NEBQJMsJ+S3yCyOdPIhM+8FUI= github.com/fluxcd/source-controller v0.0.1-beta.1 h1:1lNFrAwnrZG2qWbm9/6qgDsY4F6go+6HZ4XSCmmpAiY= github.com/fluxcd/source-controller v0.0.1-beta.1/go.mod h1:tmscNdCxEt7+Xt2g1+bI38hMPw2leYMFAaCn4UlMGuw= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -176,6 +180,7 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYis github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -186,6 +191,7 @@ github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc= github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg= github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA= @@ -313,6 +319,10 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v32 v32.0.0 h1:q74KVb22spUq0U5HqZ9VCYqQz8YRuOtL/39ZnfwO+NM= +github.com/google/go-github/v32 v32.0.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= @@ -657,6 +667,8 @@ golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -793,6 +805,7 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=