|
| 1 | +""" |
| 2 | +To consolidate all discovered OpenAPI endpoint specifications: |
| 3 | +- Ensure all required CRDs are applied on the k8s server |
| 4 | +- Preferably use `kubectl proxy` temporarily to avoid having to install certificates |
| 5 | +- Run `julia -e 'include("K8sOpenAPISpec.jl"); K8sOpenAPISpec.get_specification()'`. This should create a file named `k8s.json` with all discovered OpenAPI specifications included. |
| 6 | +
|
| 7 | +To display all discovered API versions in a generated `k8s.json` file: |
| 8 | +- Run `julia -e 'include("K8sOpenAPISpec.jl"); K8sOpenAPISpec.show_api_versions()'` |
| 9 | +""" |
| 10 | +module K8sOpenAPISpec |
| 11 | + |
| 12 | +using Downloads |
| 13 | +using JSON |
| 14 | +using YAML |
| 15 | + |
| 16 | +const DEFAULT_API_SERVER = "http://localhost:8001" |
| 17 | +const API_DISCOVERY = "/openapi/v3" |
| 18 | + |
| 19 | +function fetch_json(url::String) |
| 20 | + iob = IOBuffer() |
| 21 | + Downloads.download(url, iob) |
| 22 | + return JSON.parse(String(take!(iob))) |
| 23 | +end |
| 24 | + |
| 25 | +function discover_apis(api_server::String=DEFAULT_API_SERVER) |
| 26 | + api_discovery_url = string(api_server, API_DISCOVERY) |
| 27 | + api_discovery = fetch_json(api_discovery_url) |
| 28 | + api_discovery["paths"] |
| 29 | +end |
| 30 | + |
| 31 | +function merge_l1!(merged_spec::Dict{String,Any}, spec::Dict{String,Any}, key::String) |
| 32 | + haskey(spec, key) || return |
| 33 | + if haskey(merged_spec, key) |
| 34 | + merge!(merged_spec[key], spec[key]) |
| 35 | + else |
| 36 | + merged_spec[key] = spec[key] |
| 37 | + end |
| 38 | + return merged_spec |
| 39 | +end |
| 40 | + |
| 41 | +function merge_l2(merged_spec::Dict{String,Any}, spec::Dict{String,Any}, key1::String) |
| 42 | + haskey(spec, key1) || return |
| 43 | + |
| 44 | + if !haskey(merged_spec, key1) |
| 45 | + merged_spec[key1] = Dict{String,Any}() |
| 46 | + end |
| 47 | + |
| 48 | + for key2 in keys(spec[key1]) |
| 49 | + merge_l1!(merged_spec[key1], spec[key1], key2) |
| 50 | + end |
| 51 | + return merged_spec |
| 52 | +end |
| 53 | + |
| 54 | +function merge_array!(merged_spec::Dict{String,Any}, spec::Dict{String,Any}, key::String) |
| 55 | + if haskey(spec, key) |
| 56 | + if haskey(merged_spec, key) |
| 57 | + merged_spec[key] = vcat(merged_spec[key], spec[key]) |
| 58 | + else |
| 59 | + merged_spec[key] = spec[key] |
| 60 | + end |
| 61 | + end |
| 62 | + return merged_spec |
| 63 | +end |
| 64 | + |
| 65 | +function merge_spec!(merged_spec::Dict{String,Any}, spec::Dict{String,Any}) |
| 66 | + # /openapi |
| 67 | + haskey(spec, "openapi") && (merged_spec["openapi"] = spec["openapi"]) |
| 68 | + |
| 69 | + # /info |
| 70 | + merge_l1!(merged_spec, spec, "info") |
| 71 | + # /externalDocs |
| 72 | + merge_l1!(merged_spec, spec, "externalDocs") |
| 73 | + |
| 74 | + # /servers |
| 75 | + merge_array!(merged_spec, spec, "servers") |
| 76 | + # /tags |
| 77 | + merge_array!(merged_spec, spec, "tags") |
| 78 | + |
| 79 | + # /paths/{path}/{method} |
| 80 | + merge_l2(merged_spec, spec, "paths") |
| 81 | + |
| 82 | + # /components/schemas/{schema} |
| 83 | + # /components/responses/{response} |
| 84 | + # /components/parameters/{parameter} |
| 85 | + # /components/securitySchemes/{securityScheme} |
| 86 | + merge_l2(merged_spec, spec, "components") |
| 87 | + |
| 88 | + # /definitions/{definition} |
| 89 | + merge_l1!(merged_spec, spec, "definitions") |
| 90 | + # /securityDefinitions/{securityDefinition} |
| 91 | + merge_l1!(merged_spec, spec, "securityDefinitions") |
| 92 | + # /security (merge arrays) |
| 93 | + merge_array!(merged_spec, spec, "security") |
| 94 | + |
| 95 | + return merged_spec |
| 96 | +end |
| 97 | + |
| 98 | +function create_merged_spec(api_server::String=DEFAULT_API_SERVER) |
| 99 | + paths = discover_apis(api_server) |
| 100 | + merged_spec = Dict{String,Any}() |
| 101 | + for name in keys(paths) |
| 102 | + @info("merging $name") |
| 103 | + url = string(api_server, paths[name]["serverRelativeURL"]) |
| 104 | + api_spec = fetch_json(url) |
| 105 | + merge_spec!(merged_spec, api_spec) |
| 106 | + end |
| 107 | + if !haskey(merged_spec, "openapi") |
| 108 | + merged_spec["openapi"] = "3.0.0" |
| 109 | + end |
| 110 | + return merged_spec |
| 111 | +end |
| 112 | + |
| 113 | +""" |
| 114 | + get_specification(; outfile::String="k8s.json", api_server::String=DEFAULT_API_SERVER) |
| 115 | +
|
| 116 | +Consolidate all discovered OpenAPI endpoint specifications and write them to a file. |
| 117 | +
|
| 118 | +Keyword arguments: |
| 119 | +- `outfile::String`: The file to write the OpenAPI specification to. Defaults to `k8s.json`. |
| 120 | +- `api_server::String`: The URL of the k8s API server. Defaults to `http://localhost:8001`. |
| 121 | +""" |
| 122 | +function get_specification(; outfile::String="k8s.json", api_server::String=DEFAULT_API_SERVER) |
| 123 | + merged_spec = create_merged_spec(api_server) |
| 124 | + open(outfile, "w") do f |
| 125 | + JSON.print(f, merged_spec, 2) |
| 126 | + end |
| 127 | +end |
| 128 | + |
| 129 | +""" |
| 130 | + merge_specifications(folder::String; outfile::String="k8s.json") |
| 131 | +
|
| 132 | +Merge all JSON files in a given folder into a single OpenAPI specification. |
| 133 | +
|
| 134 | +Arguments: |
| 135 | +- `folder::String`: The folder to read the JSON files from. |
| 136 | +
|
| 137 | +Keyword arguments: |
| 138 | +- `outfile::String`: The file to write the OpenAPI specification to. Defaults to `k8s.json`. |
| 139 | +""" |
| 140 | +function merge_specifications(folder::String; outfile::String="k8s.json") |
| 141 | + merged_spec = Dict{String,Any}() |
| 142 | + for file in readdir(folder) |
| 143 | + if endswith(file, ".json") |
| 144 | + spec = JSON.parsefile(joinpath(folder, file)) |
| 145 | + elseif endswith(file, ".yaml") |
| 146 | + spec = YAML.load_file(joinpath(folder, file)) |
| 147 | + else |
| 148 | + continue |
| 149 | + end |
| 150 | + @info("merging $file") |
| 151 | + merge_spec!(merged_spec, convert(Dict{String,Any}, spec)) |
| 152 | + end |
| 153 | + if !haskey(merged_spec, "openapi") |
| 154 | + merged_spec["openapi"] = "3.0.0" |
| 155 | + end |
| 156 | + open(outfile, "w") do f |
| 157 | + JSON.print(f, merged_spec, 2) |
| 158 | + end |
| 159 | +end |
| 160 | + |
| 161 | +""" |
| 162 | + show_api_versions(specification::Dict{String,Any}) |
| 163 | +
|
| 164 | +Display all discovered API versions in a given OpenAPI specification. |
| 165 | +
|
| 166 | +Arguments: |
| 167 | +- `specification::Dict{String,Any}`: The OpenAPI specification to read the API versions from. |
| 168 | +""" |
| 169 | +function show_api_versions(specification::Dict{String,Any}) |
| 170 | + tags = Set{String}() |
| 171 | + for path in keys(specification["paths"]) |
| 172 | + for op in keys(specification["paths"][path]) |
| 173 | + if !isa(specification["paths"][path][op], Dict) |
| 174 | + # @warn("possible invalid specification: /paths/$path/$op") |
| 175 | + continue |
| 176 | + end |
| 177 | + if haskey(specification["paths"][path][op], "tags") |
| 178 | + for tag in specification["paths"][path][op]["tags"] |
| 179 | + push!(tags, tag) |
| 180 | + end |
| 181 | + end |
| 182 | + end |
| 183 | + end |
| 184 | + sorted_tags = sort(collect(tags)) |
| 185 | + for tag in sorted_tags |
| 186 | + println(tag) |
| 187 | + end |
| 188 | +end |
| 189 | + |
| 190 | +""" |
| 191 | + show_api_versions(; specification_file::String="k8s.json") |
| 192 | +
|
| 193 | +Display all discovered API versions in a generated `k8s.json` file. |
| 194 | +
|
| 195 | +Keyword arguments: |
| 196 | +- `specification_file::String`: The JSON file to read the OpenAPI specification from. Defaults to `k8s.json`. |
| 197 | +""" |
| 198 | +function show_api_versions(; specification_file::String="k8s.json") |
| 199 | + specification = JSON.parsefile(specification_file) |
| 200 | + show_api_versions(specification) |
| 201 | +end |
| 202 | + |
| 203 | +end # module K8sOpenAPISpec |
0 commit comments