@kustodian/sources 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { type KustodianErrorType, type ResultType, failure, success } from '@kustodian/core';
4
+ import { type TemplateSourceType, is_mutable_source } from '@kustodian/schema';
5
+ import { type CacheManagerType, create_cache_manager } from './cache/index.js';
6
+ import { get_fetcher_for_source } from './fetchers/index.js';
7
+ import type {
8
+ FetchOptionsType,
9
+ ResolvedSourceType,
10
+ ResolverOptionsType,
11
+ SourceResolverType,
12
+ } from './types.js';
13
+
14
+ /**
15
+ * Default cache directory relative to project root.
16
+ */
17
+ export const DEFAULT_CACHE_DIR = '.kustodian/templates-cache';
18
+
19
+ /**
20
+ * Options for creating a source resolver.
21
+ */
22
+ export interface CreateResolverOptionsType {
23
+ /** Base cache directory (defaults to .kustodian/templates-cache) */
24
+ cache_dir?: string;
25
+ }
26
+
27
+ /**
28
+ * Creates a source resolver instance.
29
+ */
30
+ export function create_source_resolver(options?: CreateResolverOptionsType): SourceResolverType {
31
+ const cache_dir = options?.cache_dir ?? path.join(process.cwd(), DEFAULT_CACHE_DIR);
32
+ const cache_manager = create_cache_manager(cache_dir);
33
+ return new SourceResolver(cache_manager);
34
+ }
35
+
36
+ class SourceResolver implements SourceResolverType {
37
+ private readonly cache: CacheManagerType;
38
+
39
+ constructor(cache_manager: CacheManagerType) {
40
+ this.cache = cache_manager;
41
+ }
42
+
43
+ async resolve_all(
44
+ sources: TemplateSourceType[],
45
+ options?: ResolverOptionsType,
46
+ ): Promise<ResultType<ResolvedSourceType[], KustodianErrorType>> {
47
+ const parallel = options?.parallel ?? true;
48
+
49
+ if (parallel) {
50
+ const results = await Promise.all(sources.map((source) => this.resolve(source, options)));
51
+
52
+ // Check for first failure
53
+ const resolved: ResolvedSourceType[] = [];
54
+ for (const result of results) {
55
+ if (!result.success) {
56
+ return failure(result.error);
57
+ }
58
+ resolved.push(result.value);
59
+ }
60
+ return success(resolved);
61
+ }
62
+
63
+ // Sequential execution
64
+ const resolved: ResolvedSourceType[] = [];
65
+ for (const source of sources) {
66
+ const result = await this.resolve(source, options);
67
+ if (!result.success) {
68
+ return failure(result.error);
69
+ }
70
+ resolved.push(result.value);
71
+ }
72
+ return success(resolved);
73
+ }
74
+
75
+ async resolve(
76
+ source: TemplateSourceType,
77
+ options?: FetchOptionsType,
78
+ ): Promise<ResultType<ResolvedSourceType, KustodianErrorType>> {
79
+ const force_refresh = options?.force_refresh ?? false;
80
+ const fetcher = get_fetcher_for_source(source);
81
+ const mutable = is_mutable_source(source);
82
+
83
+ // Determine the version to fetch
84
+ const version = this.get_source_version(source);
85
+
86
+ // Check cache first (unless force refresh)
87
+ if (!force_refresh) {
88
+ const cached = await this.cache.get(source.name, version);
89
+ if (cached.success && cached.value) {
90
+ return success({
91
+ source,
92
+ fetch_result: {
93
+ path: cached.value.path,
94
+ version: cached.value.version,
95
+ from_cache: true,
96
+ fetched_at: cached.value.fetched_at,
97
+ },
98
+ });
99
+ }
100
+ }
101
+
102
+ // Fetch from remote
103
+ const fetch_result = await fetcher.fetch(source, options);
104
+ if (!fetch_result.success) {
105
+ return fetch_result;
106
+ }
107
+
108
+ // Store in cache
109
+ const cache_result = await this.cache.put(
110
+ source.name,
111
+ fetcher.type,
112
+ fetch_result.value.version,
113
+ fetch_result.value.path,
114
+ mutable,
115
+ source.ttl,
116
+ );
117
+
118
+ if (!cache_result.success) {
119
+ // Log warning but don't fail - we have the content
120
+ console.warn(
121
+ `Warning: Failed to cache source '${source.name}': ${cache_result.error.message}`,
122
+ );
123
+ }
124
+
125
+ // If caching succeeded, use the cached path (which is more stable)
126
+ const final_path = cache_result.success ? cache_result.value.path : fetch_result.value.path;
127
+
128
+ // Cleanup temp directory if we successfully cached
129
+ if (cache_result.success && fetch_result.value.path !== cache_result.value.path) {
130
+ await fs.rm(fetch_result.value.path, { recursive: true, force: true }).catch(() => {});
131
+ }
132
+
133
+ return success({
134
+ source,
135
+ fetch_result: {
136
+ path: final_path,
137
+ version: fetch_result.value.version,
138
+ from_cache: false,
139
+ fetched_at: fetch_result.value.fetched_at,
140
+ },
141
+ });
142
+ }
143
+
144
+ async update_all(
145
+ sources: TemplateSourceType[],
146
+ ): Promise<ResultType<ResolvedSourceType[], KustodianErrorType>> {
147
+ return this.resolve_all(sources, { force_refresh: true });
148
+ }
149
+
150
+ private get_source_version(source: TemplateSourceType): string {
151
+ if (source.git) {
152
+ const ref = source.git.ref;
153
+ return ref.tag ?? ref.commit ?? ref.branch ?? 'unknown';
154
+ }
155
+ if (source.http) {
156
+ // Use checksum as version if available, otherwise use URL hash
157
+ return source.http.checksum ?? this.hash_url(source.http.url);
158
+ }
159
+ if (source.oci) {
160
+ return source.oci.digest ?? source.oci.tag ?? 'unknown';
161
+ }
162
+ return 'unknown';
163
+ }
164
+
165
+ private hash_url(url: string): string {
166
+ // Simple hash for URL-based versioning
167
+ let hash = 0;
168
+ for (let i = 0; i < url.length; i++) {
169
+ const char = url.charCodeAt(i);
170
+ hash = (hash << 5) - hash + char;
171
+ hash = hash & hash;
172
+ }
173
+ return `url-${Math.abs(hash).toString(16)}`;
174
+ }
175
+ }
package/src/types.ts ADDED
@@ -0,0 +1,194 @@
1
+ import type { KustodianErrorType, ResultType } from '@kustodian/core';
2
+ import type { TemplateSourceType } from '@kustodian/schema';
3
+
4
+ /**
5
+ * Result of a successful fetch operation.
6
+ */
7
+ export interface FetchResultType {
8
+ /** Absolute path to the fetched/extracted templates directory */
9
+ path: string;
10
+ /** Version/ref that was fetched (tag, commit, digest, etc.) */
11
+ version: string;
12
+ /** Whether this result came from cache */
13
+ from_cache: boolean;
14
+ /** Timestamp when this was fetched */
15
+ fetched_at: Date;
16
+ }
17
+
18
+ /**
19
+ * Options for fetching sources.
20
+ */
21
+ export interface FetchOptionsType {
22
+ /** Force refresh, ignoring cache */
23
+ force_refresh?: boolean;
24
+ /** Target cache directory (defaults to .kustodian/templates-cache) */
25
+ cache_dir?: string;
26
+ /** Custom timeout in milliseconds */
27
+ timeout?: number;
28
+ }
29
+
30
+ /**
31
+ * Version information from a remote source.
32
+ */
33
+ export interface RemoteVersionType {
34
+ /** Version identifier (tag name, commit sha, etc.) */
35
+ version: string;
36
+ /** Content digest if available */
37
+ digest?: string;
38
+ /** Date of the version if available */
39
+ date?: Date;
40
+ }
41
+
42
+ /**
43
+ * Cache entry metadata.
44
+ */
45
+ export interface CacheEntryType {
46
+ /** Source name from configuration */
47
+ source_name: string;
48
+ /** Source type: git, http, or oci */
49
+ source_type: 'git' | 'http' | 'oci';
50
+ /** Version/ref that was cached */
51
+ version: string;
52
+ /** Absolute path to cached content */
53
+ path: string;
54
+ /** When this was fetched */
55
+ fetched_at: Date;
56
+ /** When this expires (only for mutable refs) */
57
+ expires_at: Date | null;
58
+ /** Content checksum if available */
59
+ checksum?: string;
60
+ }
61
+
62
+ /**
63
+ * Template source fetcher interface.
64
+ * Each source type (git, http, oci) implements this interface.
65
+ */
66
+ export interface SourceFetcherType {
67
+ /** Unique identifier for this fetcher type */
68
+ readonly type: 'git' | 'http' | 'oci';
69
+
70
+ /**
71
+ * Fetches templates from the source.
72
+ * Returns path to extracted templates directory.
73
+ */
74
+ fetch(
75
+ source: TemplateSourceType,
76
+ options?: FetchOptionsType,
77
+ ): Promise<ResultType<FetchResultType, KustodianErrorType>>;
78
+
79
+ /**
80
+ * Lists available versions from the remote.
81
+ * For Git: tags and branches
82
+ * For OCI: tags
83
+ * For HTTP: not applicable (returns empty array)
84
+ */
85
+ list_versions(
86
+ source: TemplateSourceType,
87
+ ): Promise<ResultType<RemoteVersionType[], KustodianErrorType>>;
88
+
89
+ /**
90
+ * Determines if this source reference is mutable.
91
+ * Mutable refs (branches, 'latest' tag) need TTL-based refresh.
92
+ * Immutable refs (tags, commits, digests) can be cached forever.
93
+ */
94
+ is_mutable(source: TemplateSourceType): boolean;
95
+ }
96
+
97
+ /**
98
+ * Cache manager interface.
99
+ */
100
+ export interface CacheManagerType {
101
+ /** Base cache directory */
102
+ readonly cache_dir: string;
103
+
104
+ /**
105
+ * Gets a cached entry if valid (not expired for mutable refs).
106
+ */
107
+ get(
108
+ source_name: string,
109
+ version: string,
110
+ ): Promise<ResultType<CacheEntryType | null, KustodianErrorType>>;
111
+
112
+ /**
113
+ * Stores content in cache.
114
+ */
115
+ put(
116
+ source_name: string,
117
+ source_type: 'git' | 'http' | 'oci',
118
+ version: string,
119
+ content_path: string,
120
+ mutable: boolean,
121
+ ttl?: string,
122
+ ): Promise<ResultType<CacheEntryType, KustodianErrorType>>;
123
+
124
+ /**
125
+ * Invalidates a specific cache entry.
126
+ * If version is omitted, invalidates all versions for the source.
127
+ */
128
+ invalidate(source_name: string, version?: string): Promise<ResultType<void, KustodianErrorType>>;
129
+
130
+ /**
131
+ * Clears all expired entries (mutable refs past their TTL).
132
+ * Returns the number of entries removed.
133
+ */
134
+ prune(): Promise<ResultType<number, KustodianErrorType>>;
135
+
136
+ /**
137
+ * Lists all cache entries.
138
+ */
139
+ list(): Promise<ResultType<CacheEntryType[], KustodianErrorType>>;
140
+
141
+ /**
142
+ * Gets total cache size in bytes.
143
+ */
144
+ size(): Promise<ResultType<number, KustodianErrorType>>;
145
+
146
+ /**
147
+ * Clears all cached content.
148
+ */
149
+ clear(): Promise<ResultType<void, KustodianErrorType>>;
150
+ }
151
+
152
+ /**
153
+ * Resolved source with fetch result.
154
+ */
155
+ export interface ResolvedSourceType {
156
+ source: TemplateSourceType;
157
+ fetch_result: FetchResultType;
158
+ }
159
+
160
+ /**
161
+ * Source resolver options.
162
+ */
163
+ export interface ResolverOptionsType extends FetchOptionsType {
164
+ /** Run fetches in parallel (default: true) */
165
+ parallel?: boolean;
166
+ }
167
+
168
+ /**
169
+ * Source resolver interface - main entry point for fetching templates.
170
+ */
171
+ export interface SourceResolverType {
172
+ /**
173
+ * Resolves and fetches all template sources.
174
+ */
175
+ resolve_all(
176
+ sources: TemplateSourceType[],
177
+ options?: ResolverOptionsType,
178
+ ): Promise<ResultType<ResolvedSourceType[], KustodianErrorType>>;
179
+
180
+ /**
181
+ * Resolves and fetches a single source.
182
+ */
183
+ resolve(
184
+ source: TemplateSourceType,
185
+ options?: FetchOptionsType,
186
+ ): Promise<ResultType<ResolvedSourceType, KustodianErrorType>>;
187
+
188
+ /**
189
+ * Force updates all sources (ignores cache).
190
+ */
191
+ update_all(
192
+ sources: TemplateSourceType[],
193
+ ): Promise<ResultType<ResolvedSourceType[], KustodianErrorType>>;
194
+ }