Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .notice-metadata.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
92d6f614b26f27126cc196c7cc613612f372968955dfeb2d1c55d5092ea8f7a2 cli/go.sum
c7bdd90b22fa3a3837dcb7451aaf5f5796036b0a81dfb8432d1cfa41891a2786 cli/go.sum
2445cdf7f0165c1dccbe3ad8e8652b868b1e7901bc629c1d8e33c4d9890ad7c7 server/ControlPlane/packages.lock.json
8a61e6fadd8f336d262161674d0ada617ba9e295047d097adddd4cab3e036f76 server/DataPlane/packages.lock.json
201d533615b833962d871e998a69367e6ec64aaa3871555054451f760a6bfd18 scripts/generate-notice.sh
dcec8a3ad1500b77af0811f64ef252352bf0ab486e987020438d6d679992ff6e NOTICE.txt
c08e9948e8eb49b0ac81d88b73ff6aec662d6681647ff14983638c4b98ecdd44 NOTICE.txt
26 changes: 26 additions & 0 deletions NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,32 @@ SOFTWARE.

================================================================================

github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry

MIT License

Copyright (c) Microsoft Corporation. All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================================================

github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v7

MIT License
Expand Down
1 change: 1 addition & 0 deletions cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v7 v7.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0
Expand Down
2 changes: 2 additions & 0 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDo
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 h1:Hp+EScFOu9HeCbeW8WU2yQPJd4gGwhMgKxWe+G6jNzw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0/go.mod h1:/pz8dyNQe+Ey3yBp/XuYz7oqX8YDNWVpPB0hH3XWfbc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0 h1:DWlwvVV5r/Wy1561nZ3wrpI1/vDIBRY/Wd1HWaRBZWA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0/go.mod h1:E7ltexgRDmeJ0fJWv0D/HLwY2xbDdN+uv+X2uZtOx3w=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 h1:5n7dPVqsWfVKw+ZiEKSd3Kzu7gwBkbEBkeXb8rgaE9Q=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0/go.mod h1:HcZY0PHPo/7d75p99lB6lK0qYOP4vLRJUBpiehYXtLQ=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v7 v7.2.0 h1:6QEbM7ICbd8nrNtzJ5WdGl3R/ZqnUFeHhgypN+ctI/Q=
Expand Down
36 changes: 34 additions & 2 deletions cli/integrationtest/httpproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ name: http-proxy-test
services:
squid:
image: ubuntu/squid
healthcheck:
test: ["CMD", "bash", "-c", "</dev/tcp/127.0.0.1/3128"]
interval: 1s
timeout: 1s
retries: 30

tyger-proxy:
image: mcr.microsoft.com/devcontainers/base:ubuntu
Expand Down Expand Up @@ -59,6 +64,7 @@ networks:

s.CommandSucceeds("create")
s.CommandSucceeds("start", "squid")
s.WaitForHealthy("squid")

bufferId := runTygerSucceeds(t, "buffer", "create")
NewTygerCmdBuilder("buffer", "write", bufferId).Stdin("Hello").RunSucceeds(t)
Expand Down Expand Up @@ -139,7 +145,7 @@ networks:

// Connect to it from the client container
s.CommandSucceeds("start", "client")
s.ShellExecSucceeds("client", fmt.Sprintf("curl --fail --retry 5 http://tyger-proxy:6888/metadata > /dev/stderr && tyger login http://tyger-proxy:6888 && tyger buffer read --log-level trace %s > /dev/null", bufferId))
s.ShellExecSucceeds("client", fmt.Sprintf("curl --fail --retry 5 --retry-connrefused --retry-delay 1 http://tyger-proxy:6888/metadata > /dev/stderr && tyger login http://tyger-proxy:6888 && tyger buffer read --log-level trace %s > /dev/null", bufferId))

// Now repeat without TLS certificate validation

Expand All @@ -161,7 +167,7 @@ networks:
s.ShellExecSucceeds("tyger-proxy", "pgrep tyger-proxy | xargs kill && tyger-proxy start -f /creds.yml")

// Then connect to it from the client, which should use the CA certificates that the proxy publishes in its metadata
s.ShellExecSucceeds("client", fmt.Sprintf("curl --fail --retry 5 http://tyger-proxy:6888/metadata && tyger login http://tyger-proxy:6888 && tyger buffer read %s > /dev/null", bufferId))
s.ShellExecSucceeds("client", fmt.Sprintf("curl --fail --retry 5 --retry-connrefused --retry-delay 1 http://tyger-proxy:6888/metadata && tyger login http://tyger-proxy:6888 && tyger buffer read %s > /dev/null", bufferId))
}

func TestTygerProxyOverSsh(t *testing.T) {
Expand Down Expand Up @@ -331,6 +337,32 @@ func (s *ComposeSession) Command(args ...string) (stdout string, stderr string,
return b.Run()
}

func (s *ComposeSession) WaitForHealthy(service string) {
s.t.Helper()

containerId := s.CommandSucceeds("ps", "-q", service)
var lastStatus string
var lastStdErr string
var lastErr error

for attempt := 1; attempt <= 60; attempt++ {
lastStatus, lastStdErr, lastErr = runCommand("docker", "inspect", "--format", "{{.State.Health.Status}}", containerId)
if lastErr == nil && lastStatus == "healthy" {
return
}

if attempt < 60 {
time.Sleep(time.Second)
}
}

logs, _, _ := s.Command("logs", service)
if logs != "" {
s.t.Log(logs)
}
s.t.Fatalf("timed out waiting for %s to become healthy: status=%q error=%v\n%s", service, lastStatus, lastErr, lastStdErr)
}

func (s *ComposeSession) ShellExecSucceeds(service string, command string) string {
s.t.Helper()
return s.CommandSucceeds("exec", "-T", service, "bash", "-c", command)
Expand Down
229 changes: 229 additions & 0 deletions cli/internal/install/cloudinstall/acr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package cloudinstall

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"
"github.com/microsoft/tyger/cli/internal/client"
helmclient "github.com/mittwald/go-helm-client"
helmregistry "helm.sh/helm/v3/pkg/registry"
)

// Returns the *.azurecr.io host of an OCI chart reference (e.g. for
// "oci://foo.azurecr.io/helm/bar" returns "foo.azurecr.io", true). Returns
// false for non-OCI refs or registries that aren't ACRs.
func acrHostFromOciRef(ref string) (string, bool) {
if !strings.HasPrefix(ref, "oci://") {
return "", false
}
rest := strings.TrimPrefix(ref, "oci://")
host, _, _ := strings.Cut(rest, "/")
if !strings.HasSuffix(host, ".azurecr.io") {
return "", false
}
return host, true
}

// Captures the runtime-resolved properties of an Azure Container Registry:
// its short name, fully-qualified login server, and the resource group it
// lives in.
type ResolvedAcr struct {
Name string
LoginServer string
ResourceGroup string
}

type acrImportPaths struct {
SourceImage string
// Exactly one target mode is set. ACR's ImportImage API accepts repo[:tag]
// values in TargetTags, so tagged sources can be copied directly to the
// mirrored tag. Digest-pinned sources are different: the rendered manifests
// keep using repo@sha256:..., but TargetTags cannot contain a digest. For
// those, request a manifest-only copy into the target repository so the same
// digest is addressable from the mirror without inventing a tag.
TargetTag string
TargetRepositoryForDigest string
}

// Looks up the login server FQDN and resource group of the named ACR.
func (inst *Installer) resolveAcr(ctx context.Context, acrName string) (*ResolvedAcr, error) {
resourceID, err := getContainerRegistryId(ctx, acrName, inst.Config.Cloud.SubscriptionID, inst.Credential)
if err != nil {
return nil, fmt.Errorf("failed to find ACR '%s': %w", acrName, err)
}

// Resource ID format: /subscriptions/{sub}/resourceGroups/{rg}/providers/...
parts := strings.Split(resourceID, "/")
var resourceGroup string
for i, p := range parts {
if strings.EqualFold(p, "resourceGroups") && i+1 < len(parts) {
resourceGroup = parts[i+1]
break
}
}
if resourceGroup == "" {
return nil, fmt.Errorf("failed to parse resource group from ACR resource ID: %s", resourceID)
}

client, err := armcontainerregistry.NewRegistriesClient(inst.Config.Cloud.SubscriptionID, inst.Credential, nil)
if err != nil {
return nil, err
}

resp, err := client.Get(ctx, resourceGroup, acrName, nil)
if err != nil {
return nil, fmt.Errorf("failed to get ACR '%s': %w", acrName, err)
}
if resp.Properties == nil || resp.Properties.LoginServer == nil {
return nil, fmt.Errorf("ACR '%s' has no login server", acrName)
}

return &ResolvedAcr{
Name: acrName,
LoginServer: *resp.Properties.LoginServer,
ResourceGroup: resourceGroup,
}, nil
}

// Imports an image into target using the ARM ImportImage API.
func (inst *Installer) importImageToAcr(ctx context.Context, target *ResolvedAcr, sourceRegistryHost string, paths acrImportPaths) error {
client, err := armcontainerregistry.NewRegistriesClient(inst.Config.Cloud.SubscriptionID, inst.Credential, nil)
if err != nil {
return fmt.Errorf("failed to create container registry client: %w", err)
}

source, err := inst.makeImportSource(ctx, sourceRegistryHost, paths.SourceImage)
if err != nil {
return err
}

parameters := armcontainerregistry.ImportImageParameters{
Source: source,
Mode: Ptr(armcontainerregistry.ImportModeForce),
}
switch {
case paths.TargetTag != "" && paths.TargetRepositoryForDigest != "":
return fmt.Errorf("invalid ACR import target for %s: target tag and digest target repository are mutually exclusive", paths.SourceImage)
case paths.TargetTag != "":
parameters.TargetTags = []*string{Ptr(paths.TargetTag)}
case paths.TargetRepositoryForDigest != "":
parameters.UntaggedTargetRepositories = []*string{Ptr(paths.TargetRepositoryForDigest)}
default:
return fmt.Errorf("invalid ACR import target for %s: target tag or digest target repository is required", paths.SourceImage)
}

poller, err := client.BeginImportImage(ctx, target.ResourceGroup, target.Name, parameters, nil)
if err != nil {
return fmt.Errorf("failed to start import of %s/%s into '%s': %w", sourceRegistryHost, paths.SourceImage, target.Name, err)
}
if _, err := poller.PollUntilDone(ctx, nil); err != nil {
return fmt.Errorf("failed to import %s/%s into '%s': %w", sourceRegistryHost, paths.SourceImage, target.Name, err)
}

return nil
}

// Builds an ImportSource, using ResourceID for private ACRs (*.azurecr.io)
// and RegistryURI for public registries.
func (inst *Installer) makeImportSource(ctx context.Context, registryHost, sourceImage string) (*armcontainerregistry.ImportSource, error) {
if strings.HasSuffix(registryHost, ".azurecr.io") {
acrName := strings.TrimSuffix(registryHost, ".azurecr.io")
resourceID, err := getContainerRegistryId(ctx, acrName, inst.Config.Cloud.SubscriptionID, inst.Credential)
if err != nil {
return nil, fmt.Errorf("failed to get resource ID for ACR '%s': %w", acrName, err)
}
return &armcontainerregistry.ImportSource{
ResourceID: Ptr(resourceID),
SourceImage: Ptr(sourceImage),
}, nil
}
return &armcontainerregistry.ImportSource{
RegistryURI: Ptr(registryHost),
SourceImage: Ptr(sourceImage),
}, nil
}

// Logs the registry client embedded in a helmclient.Client in to the given
// ACR using an exchanged ACR refresh token.
func (inst *Installer) loginHelmClientToAcr(ctx context.Context, helmClient helmclient.Client, acrFqdn string) error {
hc, ok := helmClient.(*helmclient.HelmClient)
if !ok {
return fmt.Errorf("unable to access helm registry client for ACR login")
}
refreshToken, err := inst.getAcrRefreshToken(ctx, acrFqdn)
if err != nil {
return fmt.Errorf("failed to get ACR refresh token: %w", err)
}
if err := hc.ActionConfig.RegistryClient.Login(acrFqdn,
helmregistry.LoginOptBasicAuth("00000000-0000-0000-0000-000000000000", refreshToken)); err != nil {
return fmt.Errorf("failed to login to ACR '%s': %w", acrFqdn, err)
}
return nil
}

// Exchanges an AAD token for an ACR refresh token via the /oauth2/exchange
// endpoint.
func (inst *Installer) getAcrRefreshToken(ctx context.Context, acrFqdn string) (string, error) {
aadToken, err := inst.Credential.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{"https://management.azure.com/.default"},
})
if err != nil {
return "", fmt.Errorf("failed to get AAD token: %w", err)
}

exchangeURL := fmt.Sprintf("https://%s/oauth2/exchange", acrFqdn)
return exchangeAcrRefreshToken(ctx, client.DefaultRetryableClient.HTTPClient, exchangeURL, acrFqdn, inst.Config.Cloud.TenantID, aadToken.Token)
}
Comment thread
johnstairs marked this conversation as resolved.

type httpDoer interface {
Do(*http.Request) (*http.Response, error)
}

func exchangeAcrRefreshToken(ctx context.Context, client httpDoer, exchangeURL, acrFqdn, tenantID, aadAccessToken string) (string, error) {
formData := url.Values{}
formData.Set("grant_type", "access_token")
formData.Set("service", acrFqdn)
formData.Set("tenant", tenantID)
formData.Set("access_token", aadAccessToken)

req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL, strings.NewReader(formData.Encode()))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to exchange ACR token: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("ACR token exchange failed (status %d) and failed to read response body: %w", resp.StatusCode, err)
}
return "", fmt.Errorf("ACR token exchange failed (status %d): %s", resp.StatusCode, string(body))
}

var result struct {
RefreshToken string `json:"refresh_token"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("failed to decode ACR exchange response: %w", err)
}
if result.RefreshToken == "" {
return "", fmt.Errorf("ACR token exchange response did not include a refresh token")
}
return result.RefreshToken, nil
}
Loading
Loading