feat: Pin installed services to semver images and normalize registry versions
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
+5
-4
@@ -15,10 +15,11 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
environment:
|
environment:
|
||||||
- XDG_DATA_HOME=/data
|
- XDG_DATA_HOME=/data
|
||||||
- GITEA_REGISTRY_RO_TOKEN=
|
- GITEA_REGISTRY_RO_TOKEN=${GITEA_REGISTRY_RO_TOKEN}
|
||||||
- GITEA_RW_PACKAGE_TOKEN=
|
- GITEA_RW_PACKAGE_TOKEN=${GITEA_RW_PACKAGE_TOKEN}
|
||||||
- GITEA_ORG=
|
- GITEA_ORG=${GITEA_ORG:-Dcntrlized}
|
||||||
- GITEA_USER=
|
- GITEA_USER=${GITEA_USER:-Admin}
|
||||||
|
- GIT_TOKEN=${GIT_TOKEN}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
dcntrlized_net:
|
dcntrlized_net:
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export function App() {
|
|||||||
)}
|
)}
|
||||||
{!isRemoteOnly && container.version && (
|
{!isRemoteOnly && container.version && (
|
||||||
<div className="flex justify-between items-center py-1">
|
<div className="flex justify-between items-center py-1">
|
||||||
<span className="text-muted-foreground/80 font-medium">Installed Ver:</span>
|
<span className="text-muted-foreground/80 font-medium">Installed Version:</span>
|
||||||
<span className="font-mono text-foreground/80">{container.version}</span>
|
<span className="font-mono text-foreground/80">{container.version}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -63,12 +63,32 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) {
|
|||||||
groupedPackages[pkg.Name] = append(groupedPackages[pkg.Name], pkg)
|
groupedPackages[pkg.Name] = append(groupedPackages[pkg.Name], pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to normalize version strings for semver package (requires 'v' prefix)
|
// normalizeSemver expands partial version tags to the full X.Y.Z form,
|
||||||
formatSemver := func(v string) string {
|
// preserving an optional pre-release suffix (v3 -> 3.0.0, v3.1 -> 3.1.0,
|
||||||
if !strings.HasPrefix(v, "v") {
|
// v3.3.3-hotfix -> 3.3.3-hotfix).
|
||||||
return "v" + v
|
normalizeSemver := func(v string) string {
|
||||||
|
tag := strings.TrimPrefix(v, "v")
|
||||||
|
prerelease := ""
|
||||||
|
if i := strings.IndexByte(tag, '-'); i >= 0 {
|
||||||
|
prerelease = tag[i:]
|
||||||
|
tag = tag[:i]
|
||||||
}
|
}
|
||||||
return v
|
parts := strings.Split(tag, ".")
|
||||||
|
for len(parts) < 3 {
|
||||||
|
parts = append(parts, "0")
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ".") + prerelease
|
||||||
|
}
|
||||||
|
|
||||||
|
// semverSpecificity counts the dot-separated segments of a version's
|
||||||
|
// major.minor.patch part, so that among versions that normalize equal the most
|
||||||
|
// complete one wins (3.4.0 over 3.4).
|
||||||
|
semverSpecificity := func(v string) int {
|
||||||
|
tag := strings.TrimPrefix(v, "v")
|
||||||
|
if i := strings.IndexByte(tag, '-'); i >= 0 {
|
||||||
|
tag = tag[:i]
|
||||||
|
}
|
||||||
|
return strings.Count(tag, ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. For each container name, find the package entry with the highest semver
|
// 3. For each container name, find the package entry with the highest semver
|
||||||
@@ -79,15 +99,25 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(pkgs, func(i, j int) bool {
|
sort.Slice(pkgs, func(i, j int) bool {
|
||||||
vI := formatSemver(pkgs[i].Version)
|
nI := normalizeSemver(pkgs[i].Version)
|
||||||
vJ := formatSemver(pkgs[j].Version)
|
nJ := normalizeSemver(pkgs[j].Version)
|
||||||
|
vI := "v" + nI
|
||||||
|
vJ := "v" + nJ
|
||||||
|
|
||||||
// Compare using semver package
|
// Compare using semver package
|
||||||
if semver.IsValid(vI) && semver.IsValid(vJ) {
|
if semver.IsValid(vI) && semver.IsValid(vJ) {
|
||||||
return semver.Compare(vI, vJ) > 0
|
if c := semver.Compare(vI, vJ); c != 0 {
|
||||||
|
return c > 0
|
||||||
|
}
|
||||||
|
} else if nI != nJ {
|
||||||
|
return nI > nJ
|
||||||
}
|
}
|
||||||
// Fall back to string comparison if not valid semver
|
// Tie-break on specificity (most complete wins)
|
||||||
return pkgs[i].Version > pkgs[j].Version
|
if sI, sJ := semverSpecificity(pkgs[i].Version), semverSpecificity(pkgs[j].Version); sI != sJ {
|
||||||
|
return sI > sJ
|
||||||
|
}
|
||||||
|
// Tie-break on creation time (newest wins)
|
||||||
|
return pkgs[i].CreatedAt > pkgs[j].CreatedAt
|
||||||
})
|
})
|
||||||
|
|
||||||
// First item after descending sort is the highest version
|
// First item after descending sort is the highest version
|
||||||
@@ -115,7 +145,7 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
item := types.RegistryContainer{
|
item := types.RegistryContainer{
|
||||||
Name: pkg.Name,
|
Name: pkg.Name,
|
||||||
Version: pkg.Version,
|
Version: normalizeSemver(pkg.Version),
|
||||||
Image: imageRef,
|
Image: imageRef,
|
||||||
Source: source,
|
Source: source,
|
||||||
CreatedAt: pkg.CreatedAt,
|
CreatedAt: pkg.CreatedAt,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func StartContainer(ctx context.Context, item types.RegistryContainer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pinServiceImages(project, installedImage)
|
||||||
ensureServiceImages(ctx, dc, project, installedImage)
|
ensureServiceImages(ctx, dc, project, installedImage)
|
||||||
|
|
||||||
composeService, err := newComposeService(dc)
|
composeService, err := newComposeService(dc)
|
||||||
@@ -61,6 +62,26 @@ func StartContainer(ctx context.Context, item types.RegistryContainer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pinServiceImages rewrites each compose service's image reference to the exact
|
||||||
|
// installed image (e.g. git.../testservice:3.4.0) when its base name matches, so
|
||||||
|
// containers run pinned to a concrete semver tag instead of a moving "latest" tag.
|
||||||
|
func pinServiceImages(project *composetypes.Project, installedImage string) {
|
||||||
|
if installedImage == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for name, s := range project.Services {
|
||||||
|
if s.Image == "" || s.Image == installedImage {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if baseImageName(s.Image) != baseImageName(installedImage) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Info("Pinning service %s image %s to installed image %s", name, s.Image, installedImage)
|
||||||
|
s.Image = installedImage
|
||||||
|
project.Services[name] = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ensureServiceImages makes sure every image referenced by the compose project
|
// ensureServiceImages makes sure every image referenced by the compose project
|
||||||
// exists locally. Registries install the image under its full reference (e.g.
|
// exists locally. Registries install the image under its full reference (e.g.
|
||||||
// git.../testservice:3.4) while the service's compose file often references it
|
// git.../testservice:3.4) while the service's compose file often references it
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.Doc
|
|||||||
// Save to Cache
|
// Save to Cache
|
||||||
log.Printf("Service started. Adding container %s - %s to cache", host, containerName)
|
log.Printf("Service started. Adding container %s - %s to cache", host, containerName)
|
||||||
existing, _ := cache.services[key]
|
existing, _ := cache.services[key]
|
||||||
|
if existing.Version != "" && existing.Version != "latest" && (ver == "" || ver == "latest") {
|
||||||
|
ver = existing.Version
|
||||||
|
}
|
||||||
container := types.Container{
|
container := types.Container{
|
||||||
ID: containerID,
|
ID: containerID,
|
||||||
ServiceName: serviceLabel,
|
ServiceName: serviceLabel,
|
||||||
|
|||||||
Reference in New Issue
Block a user