Implement more of manifestutil

This commit is contained in:
Greg Neagle
2025-04-13 20:52:54 -07:00
parent 56f399ff1d
commit 63e6442a77
5 changed files with 270 additions and 50 deletions
@@ -1,46 +0,0 @@
//
// ListCatalogs.swift
// manifestutil
//
// Created by Greg Neagle on 4/13/25.
//
// Copyright 2024-2025 Greg Neagle.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import ArgumentParser
import Foundation
/// Returns a list of available catalogs
func getCatalogNames(repo: Repo) -> [String] {
do {
let catalogNames = try repo.list("catalogs")
return catalogNames.sorted()
} catch let error {
printStderr("Could not retrieve catalogs: \(error.localizedDescription)")
}
return []
}
extension ManifestUtil {
struct ListCatalogs: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Lists available catalogs in Munki repo.")
func run() throws {
let repo = try connectToRepo()
let catalogNames = getCatalogNames(repo: repo)
print(catalogNames.joined(separator: "\n"))
}
}
}
@@ -0,0 +1,108 @@
//
// MUcatalogs.swift
// manifestutil
//
// Created by Greg Neagle on 4/13/25.
//
// Copyright 2024-2025 Greg Neagle.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import ArgumentParser
import Foundation
/// Returns a list of available catalogs
func getCatalogNames(repo: Repo) throws -> [String] {
do {
let catalogNames = try repo.list("catalogs")
return catalogNames.sorted()
} catch let error {
printStderr("Could not retrieve catalogs: \(error.localizedDescription)")
throw ExitCode(-1)
}
}
/// Prints the names of the available catalogs
extension ManifestUtil {
struct ListCatalogs: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Lists available catalogs in Munki repo.")
func run() throws {
let repo = try connectToRepo()
let catalogNames = try getCatalogNames(repo: repo)
print(catalogNames.joined(separator: "\n"))
}
}
}
/// Returns a list of unique installer item (pkg) names from the given list of catalogs
func getInstallerItemNames(repo: Repo, catalogs: [String]) throws -> [String] {
var itemList = [String]()
let catalogNames = try getCatalogNames(repo: repo)
for catalogName in catalogNames {
if catalogs.contains(catalogName) {
do {
let data = try repo.get("catalogs/\(catalogName)")
if let catalog = try readPlist(fromData: data) as? [PlistDict] {
let itemNames = catalog.filter {
($0["update_for"] as? String ?? "").isEmpty &&
!(($0["name"] as? String ?? "").isEmpty)
}.map {
$0["name"] as? String ?? ""
}
itemList.append(contentsOf: itemNames)
} else {
printStderr("Catalog \(catalogName) is malformed")
}
} catch let error {
printStderr("Could not retrieve catalog: \(catalogName): \(error.localizedDescription)")
}
}
}
return Array(Set(itemList)).sorted()
}
/// Lists items in the given catalogs
extension ManifestUtil {
struct ListCatalogItems: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Lists items in the given catalogs.")
@Argument(help: ArgumentHelp(
"Catalog name",
valueName: "catalog-name"
))
var catalogNames: [String] = []
func validate() throws {
if catalogNames.isEmpty {
throw ValidationError("At least one catalog name must be provided.")
}
}
func run() throws {
let repo = try connectToRepo()
let avaliableCatalogs = try getCatalogNames(repo: repo)
for catalogName in catalogNames {
if !avaliableCatalogs.contains(catalogName) {
printStderr("Catalog '\(catalogName)' does not exist.")
throw ExitCode(-1)
}
}
let installerItemNames = try getInstallerItemNames(repo: repo, catalogs: catalogNames)
print(installerItemNames.joined(separator: "\n"))
}
}
}
@@ -0,0 +1,98 @@
//
// MUdisplayManifest.swift
// manifestutil
//
// Created by Greg Neagle on 4/13/25.
//
// Copyright 2024-2025 Greg Neagle.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import ArgumentParser
import Foundation
func getManifest(repo: Repo, name: String) -> PlistDict? {
do {
let data = try repo.get("manifests/\(name)")
return try readPlist(fromData: data) as? PlistDict
} catch {
printStderr("Could not retrieve manifest: \(error.localizedDescription)")
return nil
}
}
/// Prints a plist item in an 'attractive' way
func printPlistItem(_ label: String, _ value: Any?, indent: Int = 0) {
let INDENTSPACE = String(repeating: " ", count: indent * 4)
if let value {
if let array = value as? [Any] {
if !label.isEmpty {
print("\(INDENTSPACE)\(label):")
}
for item in array {
printPlistItem("", item, indent: indent + 1)
}
} else if let dict = value as? PlistDict {
if !label.isEmpty {
print("\(INDENTSPACE)\(label):")
}
for subkey in dict.keys.sorted() {
printPlistItem(subkey, dict[subkey], indent: indent + 1)
}
} else {
if !label.isEmpty {
print("\(INDENTSPACE)\(label): \(value)")
} else {
print("\(INDENTSPACE)\(value)")
}
}
}
}
/// Prints plist dictionary in a pretty(?) way
func printPlist(_ plist: PlistDict) {
for key in plist.keys.sorted() {
printPlistItem(key, plist[key])
}
}
/// Prints contents of a given manifest
extension ManifestUtil {
struct DisplayManifest: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Displays a manifest.")
@Flag(name: [.long, .customShort("X")],
help: "Display manifest in XML format.")
var xml: Bool = false
@Argument(help: ArgumentHelp(
"Prints the contents of the specified manifest",
valueName: "manifest-name"
))
var manifestName: String
func run() throws {
let repo = try connectToRepo()
if let manifest = getManifest(repo: repo, name: manifestName) {
if xml {
print((try? plistToString(manifest)) ?? "")
} else {
printPlist(manifest)
}
} else {
printStderr("Manifest data was malformed or not found.")
}
}
}
}
@@ -0,0 +1,59 @@
//
// MUlistManifests.swift
// manifestutil
//
// Created by Greg Neagle on 4/13/25.
//
// Copyright 2024-2025 Greg Neagle.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import ArgumentParser
import Foundation
func getManifestNames(repo: Repo) throws -> [String] {
do {
let manifestNames = try repo.list("manifests")
return manifestNames.sorted()
} catch let error {
printStderr("Could not retrieve manifests: \(error.localizedDescription)")
throw ExitCode(-1)
}
}
extension ManifestUtil {
struct ListManifests: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Lists available manifest in Munki repo.")
@Argument(help: ArgumentHelp(
"String to match manifest names similar to file name globbing. To avoid the shell expanding wildcards, wrap the string in quotes.",
valueName: "match-string"
))
var globString: String = ""
func run() throws {
let repo = try connectToRepo()
let manifestNames = try getManifestNames(repo: repo)
if globString.isEmpty {
print(manifestNames.joined(separator: "\n"))
} else {
for name in manifestNames {
if fnmatch(globString, name, 0) == 0 {
print(name)
}
}
}
}
}
}
@@ -1,5 +1,5 @@
//
// main.swift
// manifestutil.swift
// manifestutil
//
// Created by Greg Neagle on 4/13/25.
@@ -28,7 +28,7 @@ func connectToRepo() throws -> Repo {
}
var plugin = adminPref("plugin") as? String ?? "FileRepo"
if plugin.isEmpty {
plugin = "FileRepo"git
plugin = "FileRepo"
}
// connect to the repo
var repo: Repo
@@ -55,8 +55,9 @@ struct ManifestUtil: AsyncParsableCommand {
//RemoveCatalog.self,
//RemoveIncludedManifest.self,
ListCatalogs.self,
//ListCatalogItems.self,
//DisplayManifest.self,
ListCatalogItems.self,
ListManifests.self,
DisplayManifest.self,
//ExpandIncludedManifests.self,
//Find.self,
//NewManifest.self,