mirror of
https://github.com/lxc/incus.git
synced 2026-02-05 09:46:19 +01:00
408 lines
12 KiB
Go
408 lines
12 KiB
Go
package incus
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
|
|
"github.com/lxc/incus/v6/shared/api"
|
|
"github.com/lxc/incus/v6/shared/logger"
|
|
"github.com/lxc/incus/v6/shared/simplestreams"
|
|
"github.com/lxc/incus/v6/shared/util"
|
|
)
|
|
|
|
// ConnectionArgs represents a set of common connection properties.
|
|
type ConnectionArgs struct {
|
|
// TLS certificate of the remote server. If not specified, the system CA is used.
|
|
TLSServerCert string
|
|
|
|
// TLS certificate to use for client authentication.
|
|
TLSClientCert string
|
|
|
|
// TLS key to use for client authentication.
|
|
TLSClientKey string
|
|
|
|
// TLS CA to validate against when in PKI mode.
|
|
TLSCA string
|
|
|
|
// User agent string
|
|
UserAgent string
|
|
|
|
// Authentication type
|
|
AuthType string
|
|
|
|
// Custom proxy
|
|
Proxy func(*http.Request) (*url.URL, error)
|
|
|
|
// Custom HTTP Client (used as base for the connection)
|
|
HTTPClient *http.Client
|
|
|
|
// TransportWrapper wraps the *http.Transport set by Incus
|
|
TransportWrapper func(*http.Transport) HTTPTransporter
|
|
|
|
// Controls whether a client verifies the server's certificate chain and host name.
|
|
InsecureSkipVerify bool
|
|
|
|
// Cookie jar
|
|
CookieJar http.CookieJar
|
|
|
|
// OpenID Connect tokens
|
|
OIDCTokens *oidc.Tokens[*oidc.IDTokenClaims]
|
|
|
|
// Skip automatic GetServer request upon connection
|
|
SkipGetServer bool
|
|
|
|
// Caching support for image servers
|
|
CachePath string
|
|
CacheExpiry time.Duration
|
|
}
|
|
|
|
// ConnectIncus lets you connect to a remote Incus daemon over HTTPs.
|
|
//
|
|
// A client certificate (TLSClientCert) and key (TLSClientKey) must be provided.
|
|
//
|
|
// If connecting to an Incus daemon running in PKI mode, the PKI CA (TLSCA) must also be provided.
|
|
//
|
|
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
|
|
func ConnectIncus(uri string, args *ConnectionArgs) (InstanceServer, error) {
|
|
return ConnectIncusWithContext(context.Background(), uri, args)
|
|
}
|
|
|
|
// ConnectIncusWithContext lets you connect to a remote Incus daemon over HTTPs with context.Context.
|
|
//
|
|
// A client certificate (TLSClientCert) and key (TLSClientKey) must be provided.
|
|
//
|
|
// If connecting to an Incus daemon running in PKI mode, the PKI CA (TLSCA) must also be provided.
|
|
//
|
|
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
|
|
func ConnectIncusWithContext(ctx context.Context, uri string, args *ConnectionArgs) (InstanceServer, error) {
|
|
// Cleanup URL
|
|
uri = strings.TrimSuffix(uri, "/")
|
|
|
|
logger.Debug("Connecting to a remote Incus over HTTPS", logger.Ctx{"url": uri})
|
|
|
|
return httpsIncus(ctx, uri, args)
|
|
}
|
|
|
|
// ConnectIncusHTTP lets you connect to a VM agent over a VM socket.
|
|
func ConnectIncusHTTP(args *ConnectionArgs, client *http.Client) (InstanceServer, error) {
|
|
return ConnectIncusHTTPWithContext(context.Background(), args, client)
|
|
}
|
|
|
|
// ConnectIncusHTTPWithContext lets you connect to a VM agent over a VM socket with context.Context.
|
|
func ConnectIncusHTTPWithContext(ctx context.Context, args *ConnectionArgs, client *http.Client) (InstanceServer, error) {
|
|
logger.Debug("Connecting to a VM agent over a VM socket")
|
|
|
|
// Use empty args if not specified
|
|
if args == nil {
|
|
args = &ConnectionArgs{}
|
|
}
|
|
|
|
httpBaseURL, err := url.Parse("https://custom.socket")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background())
|
|
|
|
// Initialize the client struct
|
|
server := ProtocolIncus{
|
|
ctx: ctx,
|
|
httpBaseURL: *httpBaseURL,
|
|
httpProtocol: "custom",
|
|
httpUserAgent: args.UserAgent,
|
|
ctxConnected: ctxConnected,
|
|
ctxConnectedCancel: ctxConnectedCancel,
|
|
eventConns: make(map[string]*websocket.Conn),
|
|
eventListeners: make(map[string][]*EventListener),
|
|
}
|
|
|
|
// Setup the HTTP client
|
|
server.http = client
|
|
|
|
// Test the connection and seed the server information
|
|
if !args.SkipGetServer {
|
|
serverStatus, _, err := server.GetServer()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Record the server certificate
|
|
server.httpCertificate = serverStatus.Environment.Certificate
|
|
}
|
|
|
|
return &server, nil
|
|
}
|
|
|
|
// ConnectIncusUnix lets you connect to a remote Incus daemon over a local unix socket.
|
|
//
|
|
// If the path argument is empty, then $INCUS_SOCKET will be used, if
|
|
// unset $INCUS_DIR/unix.socket will be used and if that one isn't set
|
|
// either, then the path will default to /var/lib/incus/unix.socket or /run/incus/unix.socket.
|
|
func ConnectIncusUnix(path string, args *ConnectionArgs) (InstanceServer, error) {
|
|
return ConnectIncusUnixWithContext(context.Background(), path, args)
|
|
}
|
|
|
|
// ConnectIncusUnixWithContext lets you connect to a remote Incus daemon over a local unix socket with context.Context.
|
|
//
|
|
// If the path argument is empty, then $INCUS_SOCKET will be used, if
|
|
// unset $INCUS_DIR/unix.socket will be used and if that one isn't set
|
|
// either, then the path will default to /var/lib/incus/unix.socket or /run/incus/unix.socket.
|
|
func ConnectIncusUnixWithContext(ctx context.Context, path string, args *ConnectionArgs) (InstanceServer, error) {
|
|
logger.Debug("Connecting to a local Incus over a Unix socket")
|
|
|
|
// Use empty args if not specified
|
|
if args == nil {
|
|
args = &ConnectionArgs{}
|
|
}
|
|
|
|
httpBaseURL, err := url.Parse("http://unix.socket")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background())
|
|
|
|
// Determine the socket path
|
|
var projectName string
|
|
if path == "" {
|
|
path = os.Getenv("INCUS_SOCKET")
|
|
if path == "" {
|
|
incusDir := os.Getenv("INCUS_DIR")
|
|
if incusDir == "" {
|
|
_, err := os.Lstat("/run/incus/unix.socket")
|
|
if err == nil {
|
|
incusDir = "/run/incus"
|
|
} else {
|
|
incusDir = "/var/lib/incus"
|
|
}
|
|
}
|
|
|
|
path = filepath.Join(incusDir, "unix.socket")
|
|
userPath := filepath.Join(incusDir, "unix.socket.user")
|
|
if !util.PathIsWritable(path) && util.PathIsWritable(userPath) {
|
|
// Handle the use of incus-user.
|
|
path = userPath
|
|
|
|
// When using incus-user, the project list is typically restricted.
|
|
// So let's try to be smart about the project we're using.
|
|
projectName = fmt.Sprintf("user-%d", os.Geteuid())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize the client struct
|
|
server := ProtocolIncus{
|
|
ctx: ctx,
|
|
httpBaseURL: *httpBaseURL,
|
|
httpUnixPath: path,
|
|
httpProtocol: "unix",
|
|
httpUserAgent: args.UserAgent,
|
|
ctxConnected: ctxConnected,
|
|
ctxConnectedCancel: ctxConnectedCancel,
|
|
eventConns: make(map[string]*websocket.Conn),
|
|
eventListeners: make(map[string][]*EventListener),
|
|
project: projectName,
|
|
}
|
|
|
|
// Setup the HTTP client
|
|
httpClient, err := unixHTTPClient(args, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
server.http = httpClient
|
|
|
|
// Test the connection and seed the server information
|
|
if !args.SkipGetServer {
|
|
serverStatus, _, err := server.GetServer()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Record the server certificate
|
|
server.httpCertificate = serverStatus.Environment.Certificate
|
|
}
|
|
|
|
return &server, nil
|
|
}
|
|
|
|
// ConnectPublicIncus lets you connect to a remote public Incus daemon over HTTPs.
|
|
//
|
|
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
|
|
func ConnectPublicIncus(uri string, args *ConnectionArgs) (ImageServer, error) {
|
|
return ConnectPublicIncusWithContext(context.Background(), uri, args)
|
|
}
|
|
|
|
// ConnectPublicIncusWithContext lets you connect to a remote public Incus daemon over HTTPs with context.Context.
|
|
//
|
|
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
|
|
func ConnectPublicIncusWithContext(ctx context.Context, uri string, args *ConnectionArgs) (ImageServer, error) {
|
|
logger.Debug("Connecting to a remote public Incus over HTTPS")
|
|
|
|
// Cleanup URL
|
|
uri = strings.TrimSuffix(uri, "/")
|
|
|
|
return httpsIncus(ctx, uri, args)
|
|
}
|
|
|
|
// ConnectSimpleStreams lets you connect to a remote SimpleStreams image server over HTTPs.
|
|
//
|
|
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
|
|
func ConnectSimpleStreams(uri string, args *ConnectionArgs) (ImageServer, error) {
|
|
logger.Debug("Connecting to a remote simplestreams server", logger.Ctx{"URL": uri})
|
|
|
|
// Cleanup URL
|
|
uri = strings.TrimSuffix(uri, "/")
|
|
|
|
// Use empty args if not specified
|
|
if args == nil {
|
|
args = &ConnectionArgs{}
|
|
}
|
|
|
|
// Initialize the client struct
|
|
server := ProtocolSimpleStreams{
|
|
httpHost: uri,
|
|
httpUserAgent: args.UserAgent,
|
|
httpCertificate: args.TLSServerCert,
|
|
}
|
|
|
|
// Setup the HTTP client
|
|
httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.Proxy, args.TransportWrapper)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
server.http = httpClient
|
|
|
|
// Get simplestreams client
|
|
ssClient := simplestreams.NewClient(uri, *httpClient, args.UserAgent)
|
|
server.ssClient = ssClient
|
|
|
|
// Setup the cache
|
|
if args.CachePath != "" {
|
|
if !util.PathExists(args.CachePath) {
|
|
return nil, fmt.Errorf("Cache directory %q doesn't exist", args.CachePath)
|
|
}
|
|
|
|
hashedURL := fmt.Sprintf("%x", sha256.Sum256([]byte(uri)))
|
|
|
|
cachePath := filepath.Join(args.CachePath, hashedURL)
|
|
cacheExpiry := args.CacheExpiry
|
|
if cacheExpiry == 0 {
|
|
cacheExpiry = time.Hour
|
|
}
|
|
|
|
if !util.PathExists(cachePath) {
|
|
err := os.Mkdir(cachePath, 0o755)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
ssClient.SetCache(cachePath, cacheExpiry)
|
|
}
|
|
|
|
return &server, nil
|
|
}
|
|
|
|
// ConnectOCI lets you connect to a remote OCI image registry over HTTPs.
|
|
//
|
|
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
|
|
func ConnectOCI(uri string, args *ConnectionArgs) (ImageServer, error) {
|
|
logger.Debug("Connecting to a remote OCI server", logger.Ctx{"URL": uri})
|
|
|
|
// Cleanup URL
|
|
uri = strings.TrimSuffix(uri, "/")
|
|
|
|
// Use empty args if not specified
|
|
if args == nil {
|
|
args = &ConnectionArgs{}
|
|
}
|
|
|
|
// Initialize the client struct
|
|
server := ProtocolOCI{
|
|
httpHost: uri,
|
|
httpUserAgent: args.UserAgent,
|
|
httpCertificate: args.TLSServerCert,
|
|
|
|
cache: map[string]ociInfo{},
|
|
}
|
|
|
|
// Setup the HTTP client
|
|
httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.Proxy, args.TransportWrapper)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
server.http = httpClient
|
|
|
|
return &server, nil
|
|
}
|
|
|
|
// Internal function called by ConnectIncus and ConnectPublicIncus.
|
|
func httpsIncus(ctx context.Context, requestURL string, args *ConnectionArgs) (InstanceServer, error) {
|
|
// Use empty args if not specified
|
|
if args == nil {
|
|
args = &ConnectionArgs{}
|
|
}
|
|
|
|
httpBaseURL, err := url.Parse(requestURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background())
|
|
|
|
// Initialize the client struct
|
|
server := ProtocolIncus{
|
|
ctx: ctx,
|
|
httpCertificate: args.TLSServerCert,
|
|
httpBaseURL: *httpBaseURL,
|
|
httpProtocol: "https",
|
|
httpUserAgent: args.UserAgent,
|
|
ctxConnected: ctxConnected,
|
|
ctxConnectedCancel: ctxConnectedCancel,
|
|
eventConns: make(map[string]*websocket.Conn),
|
|
eventListeners: make(map[string][]*EventListener),
|
|
}
|
|
|
|
if slices.Contains([]string{api.AuthenticationMethodOIDC}, args.AuthType) {
|
|
server.RequireAuthenticated(true)
|
|
}
|
|
|
|
// Setup the HTTP client
|
|
httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.Proxy, args.TransportWrapper)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if args.CookieJar != nil {
|
|
httpClient.Jar = args.CookieJar
|
|
}
|
|
|
|
server.http = httpClient
|
|
if args.AuthType == api.AuthenticationMethodOIDC {
|
|
server.setupOIDCClient(args.OIDCTokens)
|
|
}
|
|
|
|
// Test the connection and seed the server information
|
|
if !args.SkipGetServer {
|
|
_, _, err := server.GetServer()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &server, nil
|
|
}
|