Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add certificate expiry endpoint #683

Merged
merged 5 commits into from
Sep 20, 2024
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
2 changes: 1 addition & 1 deletion src/k8s/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.22.6
require (
dario.cat/mergo v1.0.0
github.com/canonical/go-dqlite v1.22.0
github.com/canonical/k8s-snap-api v1.0.6
github.com/canonical/k8s-snap-api v1.0.7
github.com/canonical/lxd v0.0.0-20240822122218-e7b2a7a83230
github.com/canonical/microcluster/v3 v3.0.0-20240827143335-f7a4d3984970
github.com/go-logr/logr v1.4.2
Expand Down
4 changes: 2 additions & 2 deletions src/k8s/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/canonical/go-dqlite v1.22.0 h1:DuJmfcREl4gkQJyvZzjl2GHFZROhbPyfdjDRQXpkOyw=
github.com/canonical/go-dqlite v1.22.0/go.mod h1:Uvy943N8R4CFUAs59A1NVaziWY9nJ686lScY7ywurfg=
github.com/canonical/k8s-snap-api v1.0.6 h1:hUJ59ol9romwUz82bYIumitobcuBQwKjWMnge1AhGzM=
github.com/canonical/k8s-snap-api v1.0.6/go.mod h1:LDPoIYCeYnfgOFrwVPJ/4edGU264w7BB7g0GsVi36AY=
github.com/canonical/k8s-snap-api v1.0.7 h1:40qz+9IcV90ZN/wTMuOraZcuqoyRHaJck1J3c7FcWrQ=
github.com/canonical/k8s-snap-api v1.0.7/go.mod h1:LDPoIYCeYnfgOFrwVPJ/4edGU264w7BB7g0GsVi36AY=
github.com/canonical/lxd v0.0.0-20240822122218-e7b2a7a83230 h1:YOqZ+/14OPZ+/TOXpRHIX3KLT0C+wZVpewKIwlGUmW0=
github.com/canonical/lxd v0.0.0-20240822122218-e7b2a7a83230/go.mod h1:YVGI7HStOKsV+cMyXWnJ7RaMPaeWtrkxyIPvGWbgACc=
github.com/canonical/microcluster/v3 v3.0.0-20240827143335-f7a4d3984970 h1:UrnpglbXELlxtufdk6DGDytu2JzyzuS3WTsOwPrkQLI=
Expand Down
50 changes: 50 additions & 0 deletions src/k8s/pkg/k8sd/api/capi_certificates_expiry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package api

import (
"fmt"
"net/http"
"time"

apiv1 "github.com/canonical/k8s-snap-api/api/v1"
databaseutil "github.com/canonical/k8s/pkg/k8sd/database/util"
pkiutil "github.com/canonical/k8s/pkg/utils/pki"
"github.com/canonical/lxd/lxd/response"
"github.com/canonical/microcluster/v3/state"
)

func (e *Endpoints) postCertificatesExpiry(s state.State, r *http.Request) response.Response {
config, err := databaseutil.GetClusterConfig(r.Context(), s)
if err != nil {
return response.InternalError(fmt.Errorf("failed to get cluster config: %w", err))
}

certificates := []string{
config.Certificates.GetCACert(),
config.Certificates.GetClientCACert(),
config.Certificates.GetAdminClientCert(),
config.Certificates.GetAPIServerKubeletClientCert(),
config.Certificates.GetFrontProxyCACert(),
}

var earliestExpiry time.Time
// Find the earliest expiry certificate
// They should all be about the same but better double-check this.
for _, cert := range certificates {
if cert == "" {
continue
}

cert, _, err := pkiutil.LoadCertificate(cert, "")
if err != nil {
return response.InternalError(fmt.Errorf("failed to load certificate: %w", err))
}

if earliestExpiry.IsZero() || cert.NotAfter.Before(earliestExpiry) {
earliestExpiry = cert.NotAfter
}
}

return response.SyncResponse(true, &apiv1.CertificatesExpiryResponse{
ExpiryDate: earliestExpiry.Format(time.RFC3339),
})
}
5 changes: 5 additions & 0 deletions src/k8s/pkg/k8sd/api/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ func (e *Endpoints) Endpoints() []rest.Endpoint {
Path: apiv1.ClusterAPIRemoveNodeRPC,
Post: rest.EndpointAction{Handler: e.postClusterRemove, AccessHandler: ValidateCAPIAuthTokenAccessHandler("capi-auth-token"), AllowUntrusted: true},
},
{
Name: "ClusterAPI/CertificatesExpiry",
Path: apiv1.ClusterAPICertificatesExpiryRPC,
Post: rest.EndpointAction{Handler: e.postCertificatesExpiry, AccessHandler: ValidateCAPIAuthTokenAccessHandler("capi-auth-token"), AllowUntrusted: true},
},
// Snap refreshes
{
Name: "Snap/Refresh",
Expand Down
27 changes: 26 additions & 1 deletion tests/integration/tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def test_smoke(instances: List[harness.Instance]):
capture_output=True,
)
response = json.loads(resp.stdout.decode())

assert (
response["error_code"] == 0
), "Failed to generate join token using CAPI endpoints."
Expand All @@ -101,6 +100,32 @@ def test_smoke(instances: List[harness.Instance]):
metadata.get("token") is not None
), "Token not found in the generate-join-token response."

resp = instance.exec(
[
"curl",
"-XPOST",
"-H",
"Content-Type: application/json",
"-H",
"capi-auth-token: my-secret-token",
"--unix-socket",
"/var/snap/k8s/common/var/lib/k8sd/state/control.socket",
"http://localhost/1.0/x/capi/certificates-expiry",
],
capture_output=True,
)
response = json.loads(resp.stdout.decode())
assert (
response["error_code"] == 0
), "Failed to get certificate expiry using CAPI endpoints."
metadata = response.get("metadata")
assert (
metadata is not None
), "Metadata not found in the certificate expiry response."
assert util.is_valid_rfc3339(
metadata.get("expiry-date")
), "Token not found in the certificate expiry response."

def status_output_matches(p: subprocess.CompletedProcess) -> bool:
result_lines = p.stdout.decode().strip().split("\n")
if len(result_lines) != len(STATUS_PATTERNS):
Expand Down
11 changes: 11 additions & 0 deletions tests/integration/tests/test_util/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import re
import shlex
import subprocess
from datetime import datetime
from functools import partial
from pathlib import Path
from typing import Any, Callable, List, Mapping, Optional, Union
Expand Down Expand Up @@ -283,3 +284,13 @@ def get_global_unicast_ipv6(instance: harness.Instance, interface="eth0") -> str
if match:
return match.group(1)
return None


# Checks if a datastring is a valid RFC3339 date.
def is_valid_rfc3339(date_str):
try:
# Attempt to parse the string according to the RFC3339 format
datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S%z")
return True
except ValueError:
return False
Loading