DscPackageResource.cpp (20828B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "DscPackageResource.h" 5 #include "DscComposableObject.h" 6 #include "Resources.h" 7 #include "Workflows/WorkflowBase.h" 8 #include "Workflows/ConfigurationFlow.h" 9 #include "Workflows/InstallFlow.h" 10 #include "Workflows/UninstallFlow.h" 11 #include "Workflows/UpdateFlow.h" 12 #include <winget/PackageVersionSelection.h> 13 14 using namespace AppInstaller::Utility::literals; 15 using namespace AppInstaller::Repository; 16 17 namespace AppInstaller::CLI 18 { 19 namespace 20 { 21 WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(IdProperty, std::string, Identifier, "id", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, Resource::String::DscResourcePropertyDescriptionPackageId); 22 WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(SourceProperty, std::string, Source, "source", DscComposablePropertyFlag::CopyToOutput, Resource::String::DscResourcePropertyDescriptionPackageSource); 23 WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(VersionProperty, std::string, Version, "version", Resource::String::DscResourcePropertyDescriptionPackageVersion); 24 WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(MatchOptionProperty, std::string, MatchOption, "matchOption", Resource::String::DscResourcePropertyDescriptionPackageMatchOption, ({ "equals", "equalsCaseInsensitive", "startsWithCaseInsensitive", "containsCaseInsensitive" }), "equalsCaseInsensitive"); 25 WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_DEFAULT(UseLatestProperty, bool, UseLatest, "useLatest", Resource::String::DscResourcePropertyDescriptionPackageUseLatest, "false"); 26 WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(InstallModeProperty, std::string, InstallMode, "installMode", Resource::String::DscResourcePropertyDescriptionPackageInstallMode, ({ "default", "silent", "interactive" }), "silent"); 27 WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(AcceptAgreementsProperty, bool, AcceptAgreements, "acceptAgreements", Resource::String::DscResourcePropertyDescriptionAcceptAgreements); 28 29 // TODO: To support Scope on this resource: 30 // 1. Change the installed source to pull in all package info for both scopes by default 31 // 2. Change the installed source open in workflows to always open for everything, regardless of scope 32 // 3. Improve correlation handling if needed for cross-scope package installations 33 // 4. Update the test EXE installer to handle being installed for both scopes 34 using PackageResourceObject = DscComposableObject<StandardExistProperty, StandardInDesiredStateProperty, IdProperty, SourceProperty, VersionProperty, MatchOptionProperty, UseLatestProperty, InstallModeProperty, AcceptAgreementsProperty>; 35 36 std::optional<MatchType> ToMatchType(const std::optional<std::string>& value) 37 { 38 if (!value) 39 { 40 return std::nullopt; 41 } 42 43 std::string lowerValue = Utility::ToLower(value.value()); 44 45 if (lowerValue == "equals") 46 { 47 return MatchType::Exact; 48 } 49 else if (lowerValue == "equals""case""insensitive") 50 { 51 return MatchType::CaseInsensitive; 52 } 53 else if (lowerValue == "starts""with""case""insensitive") 54 { 55 return MatchType::StartsWith; 56 } 57 else if (lowerValue == "contains""case""insensitive") 58 { 59 return MatchType::Substring; 60 } 61 62 THROW_HR(E_INVALIDARG); 63 } 64 65 struct PackageFunctionData 66 { 67 PackageFunctionData(Execution::Context& context, const std::optional<Json::Value>& json, bool ignoreFieldRequirements = false) : 68 Input(json, ignoreFieldRequirements), 69 ParentContext(context) 70 { 71 Reset(); 72 } 73 74 const PackageResourceObject Input; 75 PackageResourceObject Output; 76 Execution::Context& ParentContext; 77 std::unique_ptr<Execution::Context> SubContext; 78 79 // Reset the state that is modified by Get 80 void Reset() 81 { 82 Output = Input.CopyForOutput(); 83 84 SubContext = ParentContext.CreateSubContext(); 85 SubContext->SetFlags(Execution::ContextFlag::DisableInteractivity); 86 87 if (Input.AcceptAgreements().value_or(false)) 88 { 89 SubContext->Args.AddArg(Execution::Args::Type::AcceptSourceAgreements); 90 SubContext->Args.AddArg(Execution::Args::Type::AcceptPackageAgreements); 91 } 92 } 93 94 void PrepareSubContextInputs() 95 { 96 if (Input.Source()) 97 { 98 SubContext->Args.AddArg(Execution::Args::Type::Source, Input.Source().value()); 99 } 100 } 101 102 // Fills the Output object with the current state 103 bool Get() 104 { 105 PrepareSubContextInputs(); 106 107 *SubContext << 108 Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) << 109 Workflow::OpenSource() << 110 Workflow::OpenCompositeSource(Workflow::DetermineInstalledSource(*SubContext), false, CompositeSearchBehavior::AllPackages); 111 112 if (SubContext->IsTerminated()) 113 { 114 ParentContext.Terminate(SubContext->GetTerminationHR()); 115 return false; 116 } 117 118 // Do a manual search of the now opened source 119 Source& source = SubContext->Get<Execution::Data::Source>(); 120 MatchType matchType = ToMatchType(Input.MatchOption()).value_or(MatchType::CaseInsensitive); 121 122 SearchRequest request; 123 request.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, Input.Identifier().value())); 124 125 SearchResult result = source.Search(request); 126 SubContext->Add<Execution::Data::SearchResult>(result); 127 128 if (result.Matches.empty()) 129 { 130 Output.Exist(false); 131 } 132 else if (result.Matches.size() > 1) 133 { 134 AICLI_LOG(Config, Warning, << "Found " << result.Matches.size() << " matches when searching for '" << Input.Identifier().value() << "'"); 135 Output.Exist(false); 136 } 137 else 138 { 139 auto& package = result.Matches.front().Package; 140 SubContext->Add<Execution::Data::Package>(package); 141 142 auto installedPackage = package->GetInstalled(); 143 144 // Fill Output and SubContext 145 Output.Exist(static_cast<bool>(installedPackage)); 146 Output.Identifier(package->GetProperty(PackageProperty::Id)); 147 148 if (installedPackage) 149 { 150 auto versionKeys = installedPackage->GetVersionKeys(); 151 AICLI_LOG(CLI, Verbose, << "Package::Get found " << versionKeys.size() << " installed versions"); 152 153 std::shared_ptr<Repository::IPackageVersion> installedVersion; 154 155 // Find the specific version provided if possible 156 if (Input.Version()) 157 { 158 Utility::Version inputVersion{ Input.Version().value() }; 159 160 for (const auto& key : versionKeys) 161 { 162 if (inputVersion == Utility::Version{ key.Version }) 163 { 164 installedVersion = installedPackage->GetVersion(key); 165 break; 166 } 167 } 168 } 169 170 if (!installedVersion) 171 { 172 installedVersion = installedPackage->GetLatestVersion(); 173 } 174 175 if (installedVersion) 176 { 177 Output.Version(installedVersion->GetProperty(PackageVersionProperty::Version)); 178 } 179 180 auto data = Repository::GetLatestApplicableVersion(package); 181 Output.UseLatest(!data.UpdateAvailable); 182 } 183 } 184 185 AICLI_LOG(CLI, Verbose, << "Package::Get found:\n" << Json::writeString(Json::StreamWriterBuilder{}, Output.ToJson())); 186 return true; 187 } 188 189 void Uninstall() 190 { 191 AICLI_LOG(CLI, Verbose, << "Package::Uninstall invoked"); 192 193 if (Input.Version()) 194 { 195 SubContext->Args.AddArg(Execution::Args::Type::TargetVersion, Input.Version().value()); 196 } 197 else 198 { 199 SubContext->Args.AddArg(Execution::Args::Type::AllVersions); 200 } 201 202 *SubContext << 203 Workflow::UninstallSinglePackage; 204 205 if (SubContext->IsTerminated()) 206 { 207 ParentContext.Terminate(SubContext->GetTerminationHR()); 208 return; 209 } 210 211 Output.Exist(false); 212 Output.Version(std::nullopt); 213 Output.UseLatest(std::nullopt); 214 } 215 216 void Install(bool allowDowngrade = false) 217 { 218 AICLI_LOG(CLI, Verbose, << "Package::Install invoked"); 219 220 if (Input.Version()) 221 { 222 SubContext->Args.AddArg(Execution::Args::Type::Version, Input.Version().value()); 223 } 224 225 *SubContext << 226 Workflow::SelectSinglePackageVersionForInstallOrUpgrade(Workflow::OperationType::Install, allowDowngrade) << 227 Workflow::InstallSinglePackage; 228 229 if (SubContext->IsTerminated()) 230 { 231 ParentContext.Terminate(SubContext->GetTerminationHR()); 232 return; 233 } 234 235 Output.Exist(true); 236 237 Output.Version(std::nullopt); 238 if (SubContext->Contains(Execution::Data::PackageVersion)) 239 { 240 const auto& packageVersion = SubContext->Get<Execution::Data::PackageVersion>(); 241 if (packageVersion) 242 { 243 Output.Version(packageVersion->GetProperty(Repository::PackageVersionProperty::Version)); 244 } 245 } 246 247 Output.UseLatest(std::nullopt); 248 } 249 250 void Reinstall() 251 { 252 AICLI_LOG(CLI, Verbose, << "Package::Reinstall invoked"); 253 254 SubContext->Args.AddArg(Execution::Args::Type::UninstallPrevious); 255 256 Install(true); 257 } 258 259 // Determines if the current Output values match the Input values state. 260 bool Test() 261 { 262 // Need to populate Output before calling 263 THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value()); 264 265 if (Input.ShouldExist()) 266 { 267 if (Output.Exist().value()) 268 { 269 AICLI_LOG(CLI, Verbose, << "Package::Test needed to inspect these properties: Version(" << TestVersion() << "), Latest(" << TestLatest() << ")"); 270 return TestVersion() && TestLatest(); 271 } 272 else 273 { 274 AICLI_LOG(CLI, Verbose, << "Package::Test was false because the package was not installed"); 275 return false; 276 } 277 } 278 else 279 { 280 AICLI_LOG(CLI, Verbose, << "Package::Test desired the package to not exist, and it " << (Output.Exist().value() ? "did" : "did not")); 281 return !Output.Exist().value(); 282 } 283 } 284 285 Json::Value DiffJson() 286 { 287 // Need to populate Output before calling 288 THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value()); 289 290 Json::Value result{ Json::ValueType::arrayValue }; 291 292 if (Input.ShouldExist() != Output.Exist().value()) 293 { 294 result.append(std::string{ StandardExistProperty::Name() }); 295 } 296 else 297 { 298 if (!TestVersion()) 299 { 300 result.append(std::string{ VersionProperty::Name() }); 301 } 302 303 if (!TestLatest()) 304 { 305 result.append(std::string{ UseLatestProperty::Name() }); 306 } 307 } 308 309 return result; 310 } 311 312 bool TestVersion() 313 { 314 if (Input.Version()) 315 { 316 if (Output.Version()) 317 { 318 return Utility::Version{ Input.Version().value() } == Utility::Version{ Output.Version().value() }; 319 } 320 else 321 { 322 return false; 323 } 324 } 325 else 326 { 327 return true; 328 } 329 } 330 331 bool TestLatest() 332 { 333 if (Input.UseLatest() && Input.UseLatest().value()) 334 { 335 if (Output.UseLatest()) 336 { 337 return Output.UseLatest().value(); 338 } 339 else 340 { 341 return false; 342 } 343 } 344 else 345 { 346 return true; 347 } 348 } 349 }; 350 } 351 352 DscPackageResource::DscPackageResource(std::string_view parent) : 353 DscCommandBase(parent, "package", DscResourceKind::Resource, 354 DscFunctions::Get | DscFunctions::Set | DscFunctions::Test | DscFunctions::Export | DscFunctions::Schema, 355 DscFunctionModifiers::ImplementsPretest | DscFunctionModifiers::HandlesExist | DscFunctionModifiers::ReturnsStateAndDiff) 356 { 357 } 358 359 Resource::LocString DscPackageResource::ShortDescription() const 360 { 361 return Resource::String::DscPackageResourceShortDescription; 362 } 363 364 Resource::LocString DscPackageResource::LongDescription() const 365 { 366 return Resource::String::DscPackageResourceLongDescription; 367 } 368 369 std::string DscPackageResource::ResourceType() const 370 { 371 return "Package"; 372 } 373 374 void DscPackageResource::ResourceFunctionGet(Execution::Context& context) const 375 { 376 if (auto json = GetJsonFromInput(context)) 377 { 378 PackageFunctionData data{ context, json }; 379 380 if (!data.Get()) 381 { 382 return; 383 } 384 385 WriteJsonOutputLine(context, data.Output.ToJson()); 386 } 387 } 388 389 void DscPackageResource::ResourceFunctionSet(Execution::Context& context) const 390 { 391 if (auto json = GetJsonFromInput(context)) 392 { 393 PackageFunctionData data{ context, json }; 394 395 if (!data.Get()) 396 { 397 return; 398 } 399 400 // Capture the diff before updating the output 401 auto diff = data.DiffJson(); 402 403 if (!data.Test()) 404 { 405 if (data.Input.ShouldExist()) 406 { 407 if (data.Output.Exist().value()) 408 { 409 if (!data.TestLatest()) 410 { 411 // Install will swap to update flow 412 AICLI_LOG(CLI, Info, << "Installing package to update to latest"); 413 data.Install(); 414 } 415 else // (!data.TestVersion()) 416 { 417 Utility::Version inputVersion{ data.Input.Version().value() }; 418 Utility::Version outputVersion{ data.Output.Version().value() }; 419 420 if (outputVersion < inputVersion) 421 { 422 // Install will swap to update flow 423 AICLI_LOG(CLI, Info, << "Installing package to update to desired version"); 424 data.Install(); 425 } 426 else 427 { 428 AICLI_LOG(CLI, Info, << "Reinstalling package to downgrade to desired version"); 429 data.Reinstall(); 430 } 431 } 432 } 433 else 434 { 435 AICLI_LOG(CLI, Info, << "Installing package as it was not found"); 436 data.Install(); 437 } 438 } 439 else 440 { 441 AICLI_LOG(CLI, Info, << "Uninstalling package as desired"); 442 data.Uninstall(); 443 } 444 445 if (data.SubContext->IsTerminated()) 446 { 447 return; 448 } 449 } 450 451 WriteJsonOutputLine(context, data.Output.ToJson()); 452 WriteJsonOutputLine(context, diff); 453 } 454 } 455 456 void DscPackageResource::ResourceFunctionTest(Execution::Context& context) const 457 { 458 if (auto json = GetJsonFromInput(context)) 459 { 460 PackageFunctionData data{ context, json }; 461 462 if (!data.Get()) 463 { 464 return; 465 } 466 467 data.Output.InDesiredState(data.Test()); 468 469 WriteJsonOutputLine(context, data.Output.ToJson()); 470 WriteJsonOutputLine(context, data.DiffJson()); 471 } 472 } 473 474 void DscPackageResource::ResourceFunctionExport(Execution::Context& context) const 475 { 476 auto json = GetJsonFromInput(context, false); 477 PackageFunctionData data{ context, json, true }; 478 479 data.PrepareSubContextInputs(); 480 481 if (!data.Input.UseLatest().value_or(true)) 482 { 483 data.SubContext->Args.AddArg(Execution::Args::Type::IncludeVersions); 484 } 485 486 data.SubContext->Args.AddArg(Execution::Args::Type::ConfigurationExportAll); 487 488 *data.SubContext << 489 Workflow::SearchSourceForPackageExport; 490 491 if (data.SubContext->IsTerminated()) 492 { 493 context.Terminate(data.SubContext->GetTerminationHR()); 494 return; 495 } 496 497 const auto& packageCollection = data.SubContext->Get<Execution::Data::PackageCollection>(); 498 499 for (const auto& source : packageCollection.Sources) 500 { 501 for (const auto& package : source.Packages) 502 { 503 PackageResourceObject output; 504 505 output.Identifier(package.Id); 506 output.Source(source.Details.Name); 507 508 if (!package.VersionAndChannel.GetVersion().IsEmpty()) 509 { 510 output.Version(package.VersionAndChannel.GetVersion().ToString()); 511 } 512 513 // TODO: Exporting scope requires one or more of the following: 514 // 1. Support for "OrUnknown" scope variants during Set (and workflows) 515 // 2. Tracking scope intent as we do for some other installer properties 516 // 3. Checking for the availability of the current scope in the package 517 518 WriteJsonOutputLine(context, output.ToJson()); 519 } 520 } 521 } 522 523 void DscPackageResource::ResourceFunctionSchema(Execution::Context& context) const 524 { 525 WriteJsonOutputLine(context, PackageResourceObject::Schema(ResourceType())); 526 } 527 }